@brainfile/cli 0.13.2 → 0.15.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/CHANGELOG.md +44 -0
- package/README.md +123 -358
- package/dist/cli.js +113 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/add.d.ts +3 -0
- package/dist/commands/add.d.ts.map +1 -1
- package/dist/commands/add.js +162 -3
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/adr.d.ts +22 -0
- package/dist/commands/adr.d.ts.map +1 -0
- package/dist/commands/adr.js +182 -0
- package/dist/commands/adr.js.map +1 -0
- package/dist/commands/archive.d.ts.map +1 -1
- package/dist/commands/archive.js +147 -0
- package/dist/commands/archive.js.map +1 -1
- package/dist/commands/complete.d.ts +30 -0
- package/dist/commands/complete.d.ts.map +1 -0
- package/dist/commands/complete.js +254 -0
- package/dist/commands/complete.js.map +1 -0
- package/dist/commands/contract.d.ts.map +1 -1
- package/dist/commands/contract.js +2 -0
- package/dist/commands/contract.js.map +1 -1
- package/dist/commands/delete.d.ts.map +1 -1
- package/dist/commands/delete.js +29 -1
- package/dist/commands/delete.js.map +1 -1
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +41 -8
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/list.d.ts +1 -0
- package/dist/commands/list.d.ts.map +1 -1
- package/dist/commands/list.js +34 -12
- package/dist/commands/list.js.map +1 -1
- package/dist/commands/log.d.ts +52 -0
- package/dist/commands/log.d.ts.map +1 -0
- package/dist/commands/log.js +246 -0
- package/dist/commands/log.js.map +1 -0
- package/dist/commands/mcp.d.ts.map +1 -1
- package/dist/commands/mcp.js +979 -44
- package/dist/commands/mcp.js.map +1 -1
- package/dist/commands/migrate.d.ts +4 -3
- package/dist/commands/migrate.d.ts.map +1 -1
- package/dist/commands/migrate.js +193 -38
- package/dist/commands/migrate.js.map +1 -1
- package/dist/commands/move.d.ts.map +1 -1
- package/dist/commands/move.js +90 -0
- package/dist/commands/move.js.map +1 -1
- package/dist/commands/patch.d.ts.map +1 -1
- package/dist/commands/patch.js +85 -13
- package/dist/commands/patch.js.map +1 -1
- package/dist/commands/rules.d.ts +68 -0
- package/dist/commands/rules.d.ts.map +1 -0
- package/dist/commands/rules.js +322 -0
- package/dist/commands/rules.js.map +1 -0
- package/dist/commands/schema.d.ts +31 -0
- package/dist/commands/schema.d.ts.map +1 -0
- package/dist/commands/schema.js +369 -0
- package/dist/commands/schema.js.map +1 -0
- package/dist/commands/search.d.ts +33 -0
- package/dist/commands/search.d.ts.map +1 -0
- package/dist/commands/search.js +209 -0
- package/dist/commands/search.js.map +1 -0
- package/dist/commands/show.d.ts.map +1 -1
- package/dist/commands/show.js +74 -1
- package/dist/commands/show.js.map +1 -1
- package/dist/commands/subtask.d.ts.map +1 -1
- package/dist/commands/subtask.js +72 -5
- package/dist/commands/subtask.js.map +1 -1
- package/dist/commands/types.d.ts +40 -0
- package/dist/commands/types.d.ts.map +1 -0
- package/dist/commands/types.js +242 -0
- package/dist/commands/types.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/contractRunner.d.ts.map +1 -1
- package/dist/lib/contractRunner.js +177 -12
- package/dist/lib/contractRunner.js.map +1 -1
- package/dist/schemas/base.json +159 -0
- package/dist/schemas/board.json +198 -0
- package/dist/tui/BrainfileTUI.d.ts.map +1 -1
- package/dist/tui/BrainfileTUI.js +23 -20
- package/dist/tui/BrainfileTUI.js.map +1 -1
- package/dist/tui/actions.d.ts +5 -5
- package/dist/tui/actions.d.ts.map +1 -1
- package/dist/tui/actions.js +335 -47
- package/dist/tui/actions.js.map +1 -1
- package/dist/tui/components/ArchivePanel.js +1 -1
- package/dist/tui/components/ArchivePanel.js.map +1 -1
- package/dist/tui/components/ColumnTabs.d.ts.map +1 -1
- package/dist/tui/components/ColumnTabs.js +6 -2
- package/dist/tui/components/ColumnTabs.js.map +1 -1
- package/dist/tui/components/HelpOverlay.js +3 -3
- package/dist/tui/components/HelpOverlay.js.map +1 -1
- package/dist/tui/components/LogsPanel.d.ts +16 -0
- package/dist/tui/components/LogsPanel.d.ts.map +1 -0
- package/dist/tui/components/LogsPanel.js +115 -0
- package/dist/tui/components/LogsPanel.js.map +1 -0
- package/dist/tui/components/MainPanelTabs.d.ts +2 -2
- package/dist/tui/components/MainPanelTabs.d.ts.map +1 -1
- package/dist/tui/components/MainPanelTabs.js +3 -3
- package/dist/tui/components/MainPanelTabs.js.map +1 -1
- package/dist/tui/components/StackedTaskList.d.ts +2 -1
- package/dist/tui/components/StackedTaskList.d.ts.map +1 -1
- package/dist/tui/components/StackedTaskList.js +9 -9
- package/dist/tui/components/StackedTaskList.js.map +1 -1
- package/dist/tui/components/TaskCard.d.ts +2 -1
- package/dist/tui/components/TaskCard.d.ts.map +1 -1
- package/dist/tui/components/TaskCard.js +41 -5
- package/dist/tui/components/TaskCard.js.map +1 -1
- package/dist/tui/components/TaskCardMeasure.d.ts +2 -16
- package/dist/tui/components/TaskCardMeasure.d.ts.map +1 -1
- package/dist/tui/components/TaskCardMeasure.js +30 -25
- package/dist/tui/components/TaskCardMeasure.js.map +1 -1
- package/dist/tui/components/TaskDetail.d.ts +2 -3
- package/dist/tui/components/TaskDetail.d.ts.map +1 -1
- package/dist/tui/components/TaskDetail.js +35 -12
- package/dist/tui/components/TaskDetail.js.map +1 -1
- package/dist/tui/components/TaskList.d.ts +2 -1
- package/dist/tui/components/TaskList.d.ts.map +1 -1
- package/dist/tui/components/TaskList.js +5 -5
- package/dist/tui/components/TaskList.js.map +1 -1
- package/dist/tui/components/index.d.ts +2 -2
- package/dist/tui/components/index.d.ts.map +1 -1
- package/dist/tui/components/index.js +3 -3
- package/dist/tui/components/index.js.map +1 -1
- package/dist/tui/hooks/useBrainfileLoader.d.ts.map +1 -1
- package/dist/tui/hooks/useBrainfileLoader.js +97 -31
- package/dist/tui/hooks/useBrainfileLoader.js.map +1 -1
- package/dist/tui/hooks/useKeyboardNavigation.d.ts.map +1 -1
- package/dist/tui/hooks/useKeyboardNavigation.js +47 -47
- package/dist/tui/hooks/useKeyboardNavigation.js.map +1 -1
- package/dist/tui/types.d.ts +7 -7
- package/dist/tui/types.d.ts.map +1 -1
- package/dist/utils/board-types.d.ts +13 -0
- package/dist/utils/board-types.d.ts.map +1 -0
- package/dist/utils/board-types.js +7 -0
- package/dist/utils/board-types.js.map +1 -0
- package/dist/utils/dot-brainfile.d.ts +9 -0
- package/dist/utils/dot-brainfile.d.ts.map +1 -0
- package/dist/utils/dot-brainfile.js +74 -0
- package/dist/utils/dot-brainfile.js.map +1 -0
- package/dist/utils/strict-validation.d.ts +8 -0
- package/dist/utils/strict-validation.d.ts.map +1 -0
- package/dist/utils/strict-validation.js +41 -0
- package/dist/utils/strict-validation.js.map +1 -0
- package/dist/utils/v2-detect.d.ts +28 -0
- package/dist/utils/v2-detect.d.ts.map +1 -0
- package/dist/utils/v2-detect.js +109 -0
- package/dist/utils/v2-detect.js.map +1 -0
- package/dist/utils/v2-tasks.d.ts +121 -0
- package/dist/utils/v2-tasks.d.ts.map +1 -0
- package/dist/utils/v2-tasks.js +384 -0
- package/dist/utils/v2-tasks.js.map +1 -0
- package/package.json +5 -4
- package/state.json +3 -0
package/dist/commands/mcp.js
CHANGED
|
@@ -42,12 +42,46 @@ const path = __importStar(require("path"));
|
|
|
42
42
|
const core_1 = require("@brainfile/core");
|
|
43
43
|
const errorHandler_1 = require("../utils/errorHandler");
|
|
44
44
|
const contractSpec_1 = require("../utils/contractSpec");
|
|
45
|
+
const strict_validation_1 = require("../utils/strict-validation");
|
|
45
46
|
const config_1 = require("../utils/config");
|
|
46
47
|
const github_auth_1 = require("../utils/github-auth");
|
|
47
48
|
const linear_auth_1 = require("../utils/linear-auth");
|
|
48
49
|
const core_2 = require("@brainfile/core");
|
|
49
50
|
const contractRunner_1 = require("../lib/contractRunner");
|
|
50
51
|
const archive_1 = require("../utils/archive");
|
|
52
|
+
const core_3 = require("@brainfile/core");
|
|
53
|
+
const v2_detect_1 = require("../utils/v2-detect");
|
|
54
|
+
function sanitizeTypesConfig(raw) {
|
|
55
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
56
|
+
return {};
|
|
57
|
+
}
|
|
58
|
+
const out = {};
|
|
59
|
+
for (const [name, value] of Object.entries(raw)) {
|
|
60
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const entry = value;
|
|
64
|
+
const idPrefix = typeof entry.idPrefix === 'string' && entry.idPrefix.trim()
|
|
65
|
+
? entry.idPrefix.trim()
|
|
66
|
+
: name;
|
|
67
|
+
const normalized = { idPrefix };
|
|
68
|
+
if (typeof entry.completable === 'boolean')
|
|
69
|
+
normalized.completable = entry.completable;
|
|
70
|
+
if (typeof entry.schema === 'string' && entry.schema.trim())
|
|
71
|
+
normalized.schema = entry.schema.trim();
|
|
72
|
+
out[name] = normalized;
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
function isTaskCompletable(taskType, rawTypes) {
|
|
77
|
+
const resolvedType = taskType || 'task';
|
|
78
|
+
if (resolvedType === 'task') {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
const types = sanitizeTypesConfig(rawTypes);
|
|
82
|
+
const typeConfig = types[resolvedType];
|
|
83
|
+
return typeConfig?.completable !== false;
|
|
84
|
+
}
|
|
51
85
|
function resolveBrainfile(filePath) {
|
|
52
86
|
return path.resolve(filePath);
|
|
53
87
|
}
|
|
@@ -68,6 +102,15 @@ function writeBoard(filePath, board) {
|
|
|
68
102
|
const content = core_1.Brainfile.serialize(board);
|
|
69
103
|
fs.writeFileSync(resolvedPath, content, 'utf-8');
|
|
70
104
|
}
|
|
105
|
+
function mcpStructuredError(message, field, value) {
|
|
106
|
+
return {
|
|
107
|
+
content: [{
|
|
108
|
+
type: 'text',
|
|
109
|
+
text: JSON.stringify({ error: { code: 'VALIDATION_ERROR', message, field, value } }, null, 2)
|
|
110
|
+
}],
|
|
111
|
+
isError: true
|
|
112
|
+
};
|
|
113
|
+
}
|
|
71
114
|
/**
|
|
72
115
|
* Find git repository root by walking up directory tree
|
|
73
116
|
*/
|
|
@@ -159,10 +202,37 @@ async function mcpCommand(options) {
|
|
|
159
202
|
inputSchema: {
|
|
160
203
|
file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
|
|
161
204
|
column: zod_1.z.string().optional().describe('Filter by column ID or name'),
|
|
162
|
-
tag: zod_1.z.string().optional().describe('Filter by tag')
|
|
205
|
+
tag: zod_1.z.string().optional().describe('Filter by tag'),
|
|
206
|
+
type: zod_1.z.string().optional().describe('Filter by document type (e.g., epic, adr). Only returns tasks matching this type.'),
|
|
163
207
|
}
|
|
164
|
-
}, async ({ file, column, tag }) => {
|
|
208
|
+
}, async ({ file, column, tag, type: filterType }) => {
|
|
165
209
|
const filePath = file || defaultFile;
|
|
210
|
+
// V2: use per-task files
|
|
211
|
+
if ((0, v2_detect_1.isV2)(filePath)) {
|
|
212
|
+
const board = (0, v2_detect_1.buildBoardFromV2)(filePath);
|
|
213
|
+
let tasks = [];
|
|
214
|
+
for (const col of board.columns) {
|
|
215
|
+
if (column) {
|
|
216
|
+
const matchesId = col.id === column;
|
|
217
|
+
const matchesName = col.title.toLowerCase() === column.toLowerCase();
|
|
218
|
+
if (!matchesId && !matchesName)
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
for (const task of col.tasks) {
|
|
222
|
+
if (tag && (!task.tags || !task.tags.includes(tag)))
|
|
223
|
+
continue;
|
|
224
|
+
// Filter by type: match explicit type field, or treat missing/undefined as "task"
|
|
225
|
+
if (filterType) {
|
|
226
|
+
const taskType = task.type || 'task';
|
|
227
|
+
if (taskType !== filterType)
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
tasks.push({ id: task.id, title: task.title, column: col.title, priority: task.priority, tags: task.tags, assignee: task.assignee });
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return { content: [{ type: 'text', text: JSON.stringify({ tasks, count: tasks.length }, null, 2) }] };
|
|
234
|
+
}
|
|
235
|
+
// V1: use board
|
|
166
236
|
const result = readBoard(filePath);
|
|
167
237
|
if ('error' in result) {
|
|
168
238
|
return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
|
|
@@ -170,7 +240,6 @@ async function mcpCommand(options) {
|
|
|
170
240
|
const { board } = result;
|
|
171
241
|
let tasks = [];
|
|
172
242
|
for (const col of board.columns) {
|
|
173
|
-
// Filter by column if specified
|
|
174
243
|
if (column) {
|
|
175
244
|
const matchesId = col.id === column;
|
|
176
245
|
const matchesName = col.title.toLowerCase() === column.toLowerCase();
|
|
@@ -178,9 +247,14 @@ async function mcpCommand(options) {
|
|
|
178
247
|
continue;
|
|
179
248
|
}
|
|
180
249
|
for (const task of col.tasks) {
|
|
181
|
-
// Filter by tag if specified
|
|
182
250
|
if (tag && (!task.tags || !task.tags.includes(tag)))
|
|
183
251
|
continue;
|
|
252
|
+
// Filter by type: match explicit type field, or treat missing/undefined as "task"
|
|
253
|
+
if (filterType) {
|
|
254
|
+
const taskType = task.type || 'task';
|
|
255
|
+
if (taskType !== filterType)
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
184
258
|
tasks.push({
|
|
185
259
|
id: task.id,
|
|
186
260
|
title: task.title,
|
|
@@ -206,6 +280,24 @@ async function mcpCommand(options) {
|
|
|
206
280
|
}
|
|
207
281
|
}, async ({ file, task }) => {
|
|
208
282
|
const filePath = file || defaultFile;
|
|
283
|
+
// V2: use per-task files
|
|
284
|
+
if ((0, v2_detect_1.isV2)(filePath)) {
|
|
285
|
+
const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
|
|
286
|
+
const found = (0, v2_detect_1.findV2Task)(dirs, task, true);
|
|
287
|
+
if (!found) {
|
|
288
|
+
return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
|
|
289
|
+
}
|
|
290
|
+
const { doc, isLog } = found;
|
|
291
|
+
const description = (0, v2_detect_1.extractDescription)(doc.body);
|
|
292
|
+
const output = {
|
|
293
|
+
...doc.task,
|
|
294
|
+
...(description && !doc.task.description && { description }),
|
|
295
|
+
column: isLog ? 'Completed' : (doc.task.column || 'unknown'),
|
|
296
|
+
...(isLog && { archived: true }),
|
|
297
|
+
};
|
|
298
|
+
return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
|
|
299
|
+
}
|
|
300
|
+
// V1: use board
|
|
209
301
|
const result = readBoard(filePath);
|
|
210
302
|
if ('error' in result) {
|
|
211
303
|
return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
|
|
@@ -237,6 +329,66 @@ async function mcpCommand(options) {
|
|
|
237
329
|
}
|
|
238
330
|
}, async ({ file, query, column, priority, assignee }) => {
|
|
239
331
|
const filePath = file || defaultFile;
|
|
332
|
+
// V2: search per-task files
|
|
333
|
+
if ((0, v2_detect_1.isV2)(filePath)) {
|
|
334
|
+
const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
|
|
335
|
+
const queryLower = query.toLowerCase();
|
|
336
|
+
let matches = [];
|
|
337
|
+
const taskDocs = (0, core_3.readTasksDir)(dirs.boardDir);
|
|
338
|
+
for (const doc of taskDocs) {
|
|
339
|
+
const task = doc.task;
|
|
340
|
+
if (column && task.column !== column)
|
|
341
|
+
continue;
|
|
342
|
+
if (priority && task.priority !== priority)
|
|
343
|
+
continue;
|
|
344
|
+
if (assignee && task.assignee !== assignee)
|
|
345
|
+
continue;
|
|
346
|
+
let score = 0;
|
|
347
|
+
if (task.title.toLowerCase().includes(queryLower)) {
|
|
348
|
+
score += 10;
|
|
349
|
+
if (task.title.toLowerCase().startsWith(queryLower))
|
|
350
|
+
score += 5;
|
|
351
|
+
}
|
|
352
|
+
if (task.description?.toLowerCase().includes(queryLower))
|
|
353
|
+
score += 5;
|
|
354
|
+
if (task.tags?.some(t => t.toLowerCase().includes(queryLower)))
|
|
355
|
+
score += 3;
|
|
356
|
+
if (task.id.toLowerCase() === queryLower)
|
|
357
|
+
score += 20;
|
|
358
|
+
if (score > 0) {
|
|
359
|
+
matches.push({ id: task.id, title: task.title, column: task.column, priority: task.priority, tags: task.tags, assignee: task.assignee, score });
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
// Also search logs
|
|
363
|
+
if (!column) {
|
|
364
|
+
const logDocs = (0, core_3.readTasksDir)(dirs.logsDir);
|
|
365
|
+
for (const doc of logDocs) {
|
|
366
|
+
const task = doc.task;
|
|
367
|
+
if (priority && task.priority !== priority)
|
|
368
|
+
continue;
|
|
369
|
+
if (assignee && task.assignee !== assignee)
|
|
370
|
+
continue;
|
|
371
|
+
let score = 0;
|
|
372
|
+
if (task.title.toLowerCase().includes(queryLower)) {
|
|
373
|
+
score += 10;
|
|
374
|
+
if (task.title.toLowerCase().startsWith(queryLower))
|
|
375
|
+
score += 5;
|
|
376
|
+
}
|
|
377
|
+
if (task.description?.toLowerCase().includes(queryLower))
|
|
378
|
+
score += 5;
|
|
379
|
+
if (task.tags?.some(t => t.toLowerCase().includes(queryLower)))
|
|
380
|
+
score += 3;
|
|
381
|
+
if (task.id.toLowerCase() === queryLower)
|
|
382
|
+
score += 20;
|
|
383
|
+
if (score > 0) {
|
|
384
|
+
matches.push({ id: task.id, title: task.title, column: 'Completed', priority: task.priority, tags: task.tags, assignee: task.assignee, score, isLog: true });
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
matches.sort((a, b) => b.score - a.score);
|
|
389
|
+
return { content: [{ type: 'text', text: JSON.stringify({ results: matches, count: matches.length }, null, 2) }] };
|
|
390
|
+
}
|
|
391
|
+
// V1: use board
|
|
240
392
|
const result = readBoard(filePath);
|
|
241
393
|
if ('error' in result) {
|
|
242
394
|
return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
|
|
@@ -245,7 +397,6 @@ async function mcpCommand(options) {
|
|
|
245
397
|
const queryLower = query.toLowerCase();
|
|
246
398
|
let matches = [];
|
|
247
399
|
for (const col of board.columns) {
|
|
248
|
-
// Filter by column if specified
|
|
249
400
|
if (column) {
|
|
250
401
|
const matchesId = col.id === column;
|
|
251
402
|
const matchesName = col.title.toLowerCase() === column.toLowerCase();
|
|
@@ -253,51 +404,30 @@ async function mcpCommand(options) {
|
|
|
253
404
|
continue;
|
|
254
405
|
}
|
|
255
406
|
for (const task of col.tasks) {
|
|
256
|
-
// Filter by priority if specified
|
|
257
407
|
if (priority && task.priority !== priority)
|
|
258
408
|
continue;
|
|
259
|
-
// Filter by assignee if specified
|
|
260
409
|
if (assignee && task.assignee !== assignee)
|
|
261
410
|
continue;
|
|
262
|
-
// Calculate search score
|
|
263
411
|
let score = 0;
|
|
264
|
-
// Title match (highest weight)
|
|
265
412
|
if (task.title.toLowerCase().includes(queryLower)) {
|
|
266
413
|
score += 10;
|
|
267
414
|
if (task.title.toLowerCase().startsWith(queryLower))
|
|
268
415
|
score += 5;
|
|
269
416
|
}
|
|
270
|
-
|
|
271
|
-
if (task.description?.toLowerCase().includes(queryLower)) {
|
|
417
|
+
if (task.description?.toLowerCase().includes(queryLower))
|
|
272
418
|
score += 5;
|
|
273
|
-
|
|
274
|
-
// Tag match
|
|
275
|
-
if (task.tags?.some(t => t.toLowerCase().includes(queryLower))) {
|
|
419
|
+
if (task.tags?.some(t => t.toLowerCase().includes(queryLower)))
|
|
276
420
|
score += 3;
|
|
277
|
-
|
|
278
|
-
// ID exact match
|
|
279
|
-
if (task.id.toLowerCase() === queryLower) {
|
|
421
|
+
if (task.id.toLowerCase() === queryLower)
|
|
280
422
|
score += 20;
|
|
281
|
-
}
|
|
282
423
|
if (score > 0) {
|
|
283
|
-
matches.push({
|
|
284
|
-
id: task.id,
|
|
285
|
-
title: task.title,
|
|
286
|
-
column: col.title,
|
|
287
|
-
priority: task.priority,
|
|
288
|
-
tags: task.tags,
|
|
289
|
-
assignee: task.assignee,
|
|
290
|
-
score
|
|
291
|
-
});
|
|
424
|
+
matches.push({ id: task.id, title: task.title, column: col.title, priority: task.priority, tags: task.tags, assignee: task.assignee, score });
|
|
292
425
|
}
|
|
293
426
|
}
|
|
294
427
|
}
|
|
295
|
-
// Sort by score descending
|
|
296
428
|
matches.sort((a, b) => b.score - a.score);
|
|
297
429
|
const output = { results: matches, count: matches.length };
|
|
298
|
-
return {
|
|
299
|
-
content: [{ type: 'text', text: JSON.stringify(output, null, 2) }]
|
|
300
|
-
};
|
|
430
|
+
return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
|
|
301
431
|
});
|
|
302
432
|
// Add task tool
|
|
303
433
|
server.registerTool('add_task', {
|
|
@@ -314,6 +444,7 @@ async function mcpCommand(options) {
|
|
|
314
444
|
dueDate: zod_1.z.string().optional().describe('Due date (YYYY-MM-DD)'),
|
|
315
445
|
subtasks: zod_1.z.array(zod_1.z.string()).optional().describe('Subtask titles (IDs auto-generated)'),
|
|
316
446
|
relatedFiles: zod_1.z.array(zod_1.z.string()).optional().describe('Related file paths'),
|
|
447
|
+
type: zod_1.z.string().optional().describe('Document type (e.g., epic, adr). Determines ID prefix. Default: task'),
|
|
317
448
|
// Contract creation (optional)
|
|
318
449
|
with_contract: zod_1.z.boolean().optional().describe('Attach a contract to the new task (status=ready)'),
|
|
319
450
|
deliverables: zod_1.z.array(zod_1.z.string()).optional().describe('Contract deliverables: type:path:description'),
|
|
@@ -323,14 +454,75 @@ async function mcpCommand(options) {
|
|
|
323
454
|
withContract: zod_1.z.boolean().optional().describe('Alias of with_contract'),
|
|
324
455
|
validationCommands: zod_1.z.array(zod_1.z.string()).optional().describe('Alias of validation_commands'),
|
|
325
456
|
}
|
|
326
|
-
}, async ({ file, column, title, description, priority, tags, assignee, dueDate, subtasks, relatedFiles, with_contract, deliverables, validation_commands, constraints, withContract, validationCommands, }) => {
|
|
457
|
+
}, async ({ file, column, title, description, priority, tags, assignee, dueDate, subtasks, relatedFiles, type: docType, with_contract, deliverables, validation_commands, constraints, withContract, validationCommands, }) => {
|
|
327
458
|
const filePath = file || defaultFile;
|
|
459
|
+
// V2: add task as individual file
|
|
460
|
+
if ((0, v2_detect_1.isV2)(filePath)) {
|
|
461
|
+
try {
|
|
462
|
+
const dirs = (0, v2_detect_1.ensureV2Dirs)(filePath);
|
|
463
|
+
const board = (0, v2_detect_1.readV2BoardConfig)(filePath);
|
|
464
|
+
const typePrefix = docType || 'task';
|
|
465
|
+
const typeValidation = (0, strict_validation_1.validateType)(board, typePrefix);
|
|
466
|
+
if (!typeValidation.valid) {
|
|
467
|
+
return mcpStructuredError(typeValidation.error || `Invalid type: ${typePrefix}`, 'type', typePrefix);
|
|
468
|
+
}
|
|
469
|
+
let targetColumn = board.columns.find(c => c.id === column);
|
|
470
|
+
if (!targetColumn)
|
|
471
|
+
targetColumn = board.columns.find(c => c.title.toLowerCase() === column.toLowerCase());
|
|
472
|
+
if (!targetColumn) {
|
|
473
|
+
const available = board.columns.map(c => `${c.id} (${c.title})`).join(', ');
|
|
474
|
+
return { content: [{ type: 'text', text: `Error: Column not found: ${column}. Available: ${available}` }], isError: true };
|
|
475
|
+
}
|
|
476
|
+
const taskId = (0, core_3.generateNextFileTaskId)(dirs.boardDir, dirs.logsDir, typePrefix);
|
|
477
|
+
const existingTasks = (0, core_3.readTasksDir)(dirs.boardDir).filter(t => t.task.column === targetColumn.id);
|
|
478
|
+
const position = existingTasks.length;
|
|
479
|
+
const builtSubtasks = subtasks && subtasks.length > 0
|
|
480
|
+
? subtasks.map((st, i) => ({ id: `${taskId}-${i + 1}`, title: st.trim(), completed: false }))
|
|
481
|
+
: undefined;
|
|
482
|
+
const task = {
|
|
483
|
+
id: taskId,
|
|
484
|
+
title,
|
|
485
|
+
...(docType && docType !== 'task' && { type: docType }),
|
|
486
|
+
column: targetColumn.id,
|
|
487
|
+
position,
|
|
488
|
+
...(description && { description }),
|
|
489
|
+
...(priority && { priority }),
|
|
490
|
+
...(tags && tags.length > 0 && { tags }),
|
|
491
|
+
...(assignee && { assignee }),
|
|
492
|
+
...(dueDate && { dueDate }),
|
|
493
|
+
...(relatedFiles && relatedFiles.length > 0 && { relatedFiles }),
|
|
494
|
+
...(builtSubtasks && { subtasks: builtSubtasks }),
|
|
495
|
+
createdAt: new Date().toISOString(),
|
|
496
|
+
};
|
|
497
|
+
// Optionally attach contract
|
|
498
|
+
const wantsContract = Boolean(with_contract ?? withContract) ||
|
|
499
|
+
Boolean(deliverables && deliverables.length > 0) ||
|
|
500
|
+
Boolean(validation_commands && validation_commands.length > 0) ||
|
|
501
|
+
Boolean(validationCommands && validationCommands.length > 0) ||
|
|
502
|
+
Boolean(constraints && constraints.length > 0);
|
|
503
|
+
if (wantsContract) {
|
|
504
|
+
const contract = (0, contractSpec_1.buildContract)({
|
|
505
|
+
deliverableSpecs: deliverables,
|
|
506
|
+
validationCommands: validation_commands ?? validationCommands,
|
|
507
|
+
constraints,
|
|
508
|
+
});
|
|
509
|
+
task.contract = contract;
|
|
510
|
+
}
|
|
511
|
+
const taskPath = path.join(dirs.boardDir, (0, core_3.taskFileName)(taskId));
|
|
512
|
+
const body = description ? (0, v2_detect_1.composeBody)(description) : '';
|
|
513
|
+
(0, core_3.writeTaskFile)(taskPath, task, body);
|
|
514
|
+
return { content: [{ type: 'text', text: `Task added successfully: ${taskId} - ${title}` }] };
|
|
515
|
+
}
|
|
516
|
+
catch (e) {
|
|
517
|
+
return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
// V1: use board
|
|
328
521
|
const result = readBoard(filePath);
|
|
329
522
|
if ('error' in result) {
|
|
330
523
|
return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
|
|
331
524
|
}
|
|
332
525
|
let { board } = result;
|
|
333
|
-
// Find target column
|
|
334
526
|
let targetColumn = (0, core_1.findColumnById)(board, column);
|
|
335
527
|
if (!targetColumn) {
|
|
336
528
|
targetColumn = (0, core_1.findColumnByName)(board, column);
|
|
@@ -353,11 +545,9 @@ async function mcpCommand(options) {
|
|
|
353
545
|
if (!addResult.success) {
|
|
354
546
|
return { content: [{ type: 'text', text: `Error: ${addResult.error}` }], isError: true };
|
|
355
547
|
}
|
|
356
|
-
// Get the new task
|
|
357
548
|
const newTask = addResult.board.columns
|
|
358
549
|
.find(c => c.id === targetColumn.id)
|
|
359
550
|
.tasks.slice(-1)[0];
|
|
360
|
-
// Optionally attach a contract (status=ready)
|
|
361
551
|
const wantsContract = Boolean(with_contract ?? withContract) ||
|
|
362
552
|
Boolean(deliverables && deliverables.length > 0) ||
|
|
363
553
|
Boolean(validation_commands && validation_commands.length > 0) ||
|
|
@@ -405,6 +595,29 @@ async function mcpCommand(options) {
|
|
|
405
595
|
if (!resolvedTaskId) {
|
|
406
596
|
return { content: [{ type: 'text', text: 'Error: task is required' }], isError: true };
|
|
407
597
|
}
|
|
598
|
+
// V2: update task file directly
|
|
599
|
+
if ((0, v2_detect_1.isV2)(filePath)) {
|
|
600
|
+
const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
|
|
601
|
+
const found = (0, v2_detect_1.findV2Task)(dirs, resolvedTaskId);
|
|
602
|
+
if (!found) {
|
|
603
|
+
return { content: [{ type: 'text', text: `Error: Task not found: ${resolvedTaskId}` }], isError: true };
|
|
604
|
+
}
|
|
605
|
+
try {
|
|
606
|
+
const contract = (0, contractSpec_1.buildContract)({
|
|
607
|
+
deliverableSpecs: deliverables,
|
|
608
|
+
validationCommands: validation_commands ?? validationCommands,
|
|
609
|
+
constraints,
|
|
610
|
+
});
|
|
611
|
+
found.doc.task.contract = contract;
|
|
612
|
+
found.doc.task.updatedAt = new Date().toISOString();
|
|
613
|
+
(0, core_3.writeTaskFile)(found.filePath, found.doc.task, found.doc.body);
|
|
614
|
+
return { content: [{ type: 'text', text: `Contract attached: ${resolvedTaskId}` }] };
|
|
615
|
+
}
|
|
616
|
+
catch (e) {
|
|
617
|
+
return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
// V1: use board
|
|
408
621
|
const result = readBoard(filePath);
|
|
409
622
|
if ('error' in result) {
|
|
410
623
|
return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
|
|
@@ -442,17 +655,56 @@ async function mcpCommand(options) {
|
|
|
442
655
|
}
|
|
443
656
|
}, async ({ file, task, column }) => {
|
|
444
657
|
const filePath = file || defaultFile;
|
|
658
|
+
// V2: update task file
|
|
659
|
+
if ((0, v2_detect_1.isV2)(filePath)) {
|
|
660
|
+
const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
|
|
661
|
+
const taskPath = path.join(dirs.boardDir, (0, core_3.taskFileName)(task));
|
|
662
|
+
const doc = (0, core_3.readTaskFile)(taskPath);
|
|
663
|
+
if (!doc) {
|
|
664
|
+
return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
|
|
665
|
+
}
|
|
666
|
+
const board = (0, v2_detect_1.readV2BoardConfig)(filePath);
|
|
667
|
+
let targetColumn = board.columns.find(c => c.id === column);
|
|
668
|
+
if (!targetColumn)
|
|
669
|
+
targetColumn = board.columns.find(c => c.title.toLowerCase() === column.toLowerCase());
|
|
670
|
+
const targetColumnId = targetColumn?.id || column;
|
|
671
|
+
const columnValidation = (0, strict_validation_1.validateColumn)(board, targetColumnId);
|
|
672
|
+
if (!columnValidation.valid) {
|
|
673
|
+
return mcpStructuredError(columnValidation.error || `Invalid column: ${targetColumnId}`, 'column', targetColumnId);
|
|
674
|
+
}
|
|
675
|
+
const resolvedTargetColumn = targetColumn || { id: column, title: column, tasks: [] };
|
|
676
|
+
const sourceColumn = doc.task.column || '';
|
|
677
|
+
const targetTasks = (0, core_3.readTasksDir)(dirs.boardDir).filter(t => t.task.column === resolvedTargetColumn.id);
|
|
678
|
+
doc.task.column = resolvedTargetColumn.id;
|
|
679
|
+
doc.task.position = targetTasks.length;
|
|
680
|
+
doc.task.updatedAt = new Date().toISOString();
|
|
681
|
+
(0, core_3.writeTaskFile)(taskPath, doc.task, doc.body);
|
|
682
|
+
const shouldAutoComplete = resolvedTargetColumn.completionColumn === true && isTaskCompletable(doc.task.type, board.types);
|
|
683
|
+
if (shouldAutoComplete) {
|
|
684
|
+
const completeResult = (0, core_3.completeTaskFile)(taskPath, dirs.logsDir);
|
|
685
|
+
if (!completeResult.success) {
|
|
686
|
+
return { content: [{ type: 'text', text: `Error: ${completeResult.error || `Failed to complete task: ${task}`}` }], isError: true };
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
const warning = (0, errorHandler_1.mcpCheckIncompleteSubtasks)(doc.task, resolvedTargetColumn);
|
|
690
|
+
let message = `Task ${task} moved from "${sourceColumn}" to "${resolvedTargetColumn.title}"`;
|
|
691
|
+
if (shouldAutoComplete) {
|
|
692
|
+
message += '\nTask auto-completed and moved to logs/.';
|
|
693
|
+
}
|
|
694
|
+
if (warning)
|
|
695
|
+
message += `\n\n${warning.warning}`;
|
|
696
|
+
return { content: [{ type: 'text', text: message }] };
|
|
697
|
+
}
|
|
698
|
+
// V1: use board
|
|
445
699
|
const result = readBoard(filePath);
|
|
446
700
|
if ('error' in result) {
|
|
447
701
|
return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
|
|
448
702
|
}
|
|
449
703
|
let { board } = result;
|
|
450
|
-
// Find the task
|
|
451
704
|
const taskInfo = (0, core_1.findTaskById)(board, task);
|
|
452
705
|
if (!taskInfo) {
|
|
453
706
|
return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
|
|
454
707
|
}
|
|
455
|
-
// Find target column
|
|
456
708
|
let targetColumn = (0, core_1.findColumnById)(board, column);
|
|
457
709
|
if (!targetColumn) {
|
|
458
710
|
targetColumn = (0, core_1.findColumnByName)(board, column);
|
|
@@ -465,7 +717,6 @@ async function mcpCommand(options) {
|
|
|
465
717
|
return { content: [{ type: 'text', text: `Error: ${moveResult.error}` }], isError: true };
|
|
466
718
|
}
|
|
467
719
|
writeBoard(filePath, moveResult.board);
|
|
468
|
-
// Check for incomplete subtasks warning when moving to done-like column
|
|
469
720
|
const warning = (0, errorHandler_1.mcpCheckIncompleteSubtasks)(taskInfo.task, targetColumn);
|
|
470
721
|
let message = `Task ${task} moved from "${taskInfo.column.title}" to "${targetColumn.title}"`;
|
|
471
722
|
if (warning) {
|
|
@@ -492,13 +743,64 @@ async function mcpCommand(options) {
|
|
|
492
743
|
}
|
|
493
744
|
}, async ({ file, task, title, description, priority, tags, assignee, dueDate, relatedFiles }) => {
|
|
494
745
|
const filePath = file || defaultFile;
|
|
746
|
+
const isNull = (v) => v === null || v === 'null';
|
|
747
|
+
// V2: update task file directly
|
|
748
|
+
if ((0, v2_detect_1.isV2)(filePath)) {
|
|
749
|
+
const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
|
|
750
|
+
const taskPath = path.join(dirs.boardDir, (0, core_3.taskFileName)(task));
|
|
751
|
+
const doc = (0, core_3.readTaskFile)(taskPath);
|
|
752
|
+
if (!doc) {
|
|
753
|
+
return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
|
|
754
|
+
}
|
|
755
|
+
const t = doc.task;
|
|
756
|
+
if (title !== undefined)
|
|
757
|
+
t.title = title;
|
|
758
|
+
if (description !== undefined) {
|
|
759
|
+
if (isNull(description))
|
|
760
|
+
delete t.description;
|
|
761
|
+
else
|
|
762
|
+
t.description = description;
|
|
763
|
+
}
|
|
764
|
+
if (priority !== undefined) {
|
|
765
|
+
if (isNull(priority))
|
|
766
|
+
delete t.priority;
|
|
767
|
+
else
|
|
768
|
+
t.priority = priority;
|
|
769
|
+
}
|
|
770
|
+
if (tags !== undefined) {
|
|
771
|
+
if (isNull(tags))
|
|
772
|
+
delete t.tags;
|
|
773
|
+
else
|
|
774
|
+
t.tags = tags;
|
|
775
|
+
}
|
|
776
|
+
if (assignee !== undefined) {
|
|
777
|
+
if (isNull(assignee))
|
|
778
|
+
delete t.assignee;
|
|
779
|
+
else
|
|
780
|
+
t.assignee = assignee;
|
|
781
|
+
}
|
|
782
|
+
if (dueDate !== undefined) {
|
|
783
|
+
if (isNull(dueDate))
|
|
784
|
+
delete t.dueDate;
|
|
785
|
+
else
|
|
786
|
+
t.dueDate = dueDate;
|
|
787
|
+
}
|
|
788
|
+
if (relatedFiles !== undefined) {
|
|
789
|
+
if (isNull(relatedFiles))
|
|
790
|
+
delete t.relatedFiles;
|
|
791
|
+
else
|
|
792
|
+
t.relatedFiles = relatedFiles;
|
|
793
|
+
}
|
|
794
|
+
t.updatedAt = new Date().toISOString();
|
|
795
|
+
(0, core_3.writeTaskFile)(taskPath, t, doc.body);
|
|
796
|
+
return { content: [{ type: 'text', text: `Task ${task} updated successfully` }] };
|
|
797
|
+
}
|
|
798
|
+
// V1: use board
|
|
495
799
|
const result = readBoard(filePath);
|
|
496
800
|
if ('error' in result) {
|
|
497
801
|
return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
|
|
498
802
|
}
|
|
499
803
|
let { board } = result;
|
|
500
|
-
// Helper to check for null or "null" string (MCP clients may send either)
|
|
501
|
-
const isNull = (v) => v === null || v === 'null';
|
|
502
804
|
const patch = {};
|
|
503
805
|
if (title !== undefined)
|
|
504
806
|
patch.title = title;
|
|
@@ -533,6 +835,22 @@ async function mcpCommand(options) {
|
|
|
533
835
|
}
|
|
534
836
|
}, async ({ file, task }) => {
|
|
535
837
|
const filePath = file || defaultFile;
|
|
838
|
+
// V2: delete task file
|
|
839
|
+
if ((0, v2_detect_1.isV2)(filePath)) {
|
|
840
|
+
const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
|
|
841
|
+
const found = (0, v2_detect_1.findV2Task)(dirs, task, true);
|
|
842
|
+
if (!found) {
|
|
843
|
+
return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
|
|
844
|
+
}
|
|
845
|
+
try {
|
|
846
|
+
fs.unlinkSync(found.filePath);
|
|
847
|
+
return { content: [{ type: 'text', text: `Task ${task} deleted successfully` }] };
|
|
848
|
+
}
|
|
849
|
+
catch (e) {
|
|
850
|
+
return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
// V1: use board
|
|
536
854
|
const result = readBoard(filePath);
|
|
537
855
|
if ('error' in result) {
|
|
538
856
|
return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
|
|
@@ -563,6 +881,78 @@ async function mcpCommand(options) {
|
|
|
563
881
|
}
|
|
564
882
|
}, async ({ file, task, destination }) => {
|
|
565
883
|
const filePath = file || defaultFile;
|
|
884
|
+
// V2: archive means move task from board/ to logs/
|
|
885
|
+
if ((0, v2_detect_1.isV2)(filePath)) {
|
|
886
|
+
const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
|
|
887
|
+
const found = (0, v2_detect_1.findV2Task)(dirs, task);
|
|
888
|
+
if (!found) {
|
|
889
|
+
return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
|
|
890
|
+
}
|
|
891
|
+
// Determine effective destination
|
|
892
|
+
const board = (0, v2_detect_1.readV2BoardConfig)(filePath);
|
|
893
|
+
const brainfileDestination = board.archive?.destination;
|
|
894
|
+
const effectiveDestination = destination || (0, config_1.getEffectiveArchiveDestination)(brainfileDestination);
|
|
895
|
+
// For GitHub/Linear, format and send before removing
|
|
896
|
+
if (effectiveDestination === 'github') {
|
|
897
|
+
if (!(await (0, github_auth_1.isGitHubAuthenticated)())) {
|
|
898
|
+
return { content: [{ type: 'text', text: `Error: Not authenticated with GitHub.\n\nTo authenticate, run:\n npx @brainfile/cli auth github\n\nOr fall back to local archive:\n Use destination: "local"` }], isError: true };
|
|
899
|
+
}
|
|
900
|
+
const config = (0, config_1.getArchiveConfig)();
|
|
901
|
+
if (!config.github?.owner || !config.github?.repo) {
|
|
902
|
+
return { content: [{ type: 'text', text: `Error: GitHub repository not configured.\n\nTo configure, run:\n npx @brainfile/cli config set archive.github.owner <owner>\n npx @brainfile/cli config set archive.github.repo <repo>` }], isError: true };
|
|
903
|
+
}
|
|
904
|
+
const payload = (0, core_2.formatTaskForGitHub)(found.doc.task, {
|
|
905
|
+
includeMeta: true, includeSubtasks: true, includeRelatedFiles: true,
|
|
906
|
+
boardTitle: board.title, fromColumn: found.doc.task.column || 'unknown',
|
|
907
|
+
extraLabels: config.github.labels,
|
|
908
|
+
});
|
|
909
|
+
const ghResult = await (0, github_auth_1.createGitHubIssue)({ owner: config.github.owner, repo: config.github.repo, title: payload.title, body: payload.body, labels: payload.labels, state: 'closed' });
|
|
910
|
+
if (!ghResult.success) {
|
|
911
|
+
return { content: [{ type: 'text', text: `Error creating GitHub issue: ${ghResult.error}` }], isError: true };
|
|
912
|
+
}
|
|
913
|
+
fs.unlinkSync(found.filePath);
|
|
914
|
+
return { content: [{ type: 'text', text: `Task ${task} archived to GitHub Issue #${ghResult.issueNumber} (closed)\n\nView: ${ghResult.issueUrl}` }] };
|
|
915
|
+
}
|
|
916
|
+
if (effectiveDestination === 'linear') {
|
|
917
|
+
if (!(await (0, linear_auth_1.isLinearAuthenticated)())) {
|
|
918
|
+
return { content: [{ type: 'text', text: `Error: Not authenticated with Linear.\n\nTo authenticate, run:\n npx @brainfile/cli auth linear --token <api-key>` }], isError: true };
|
|
919
|
+
}
|
|
920
|
+
const config = (0, config_1.getArchiveConfig)();
|
|
921
|
+
let teamId = config.linear?.teamId;
|
|
922
|
+
if (!teamId) {
|
|
923
|
+
const teams = await (0, linear_auth_1.getLinearTeams)();
|
|
924
|
+
if (teams.length === 0) {
|
|
925
|
+
return { content: [{ type: 'text', text: `Error: No Linear teams found.` }], isError: true };
|
|
926
|
+
}
|
|
927
|
+
if (teams.length === 1) {
|
|
928
|
+
teamId = teams[0].id;
|
|
929
|
+
}
|
|
930
|
+
else {
|
|
931
|
+
const teamList = teams.map(t => ` ${t.key}: ${t.name} (${t.id})`).join('\n');
|
|
932
|
+
return { content: [{ type: 'text', text: `Error: Multiple Linear teams found. Please configure a default.\n\nAvailable teams:\n${teamList}` }], isError: true };
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
const payload = (0, core_2.formatTaskForLinear)(found.doc.task, {
|
|
936
|
+
includeMeta: true, includeSubtasks: true, includeRelatedFiles: true,
|
|
937
|
+
boardTitle: board.title, fromColumn: found.doc.task.column || 'unknown', stateName: 'Done',
|
|
938
|
+
});
|
|
939
|
+
const linearResult = await (0, linear_auth_1.createLinearIssue)({ teamId, title: payload.title, description: payload.description, priority: payload.priority, labelNames: payload.labelNames, stateName: 'Done' });
|
|
940
|
+
if (!linearResult.success) {
|
|
941
|
+
return { content: [{ type: 'text', text: `Error creating Linear issue: ${linearResult.error}` }], isError: true };
|
|
942
|
+
}
|
|
943
|
+
fs.unlinkSync(found.filePath);
|
|
944
|
+
return { content: [{ type: 'text', text: `Task ${task} archived to Linear Issue ${linearResult.issueId} (Done)\n\nView: ${linearResult.issueUrl}` }] };
|
|
945
|
+
}
|
|
946
|
+
// Local archive: move from board/ to logs/
|
|
947
|
+
const logPath = path.join(dirs.logsDir, (0, core_3.taskFileName)(task));
|
|
948
|
+
found.doc.task.completedAt = found.doc.task.completedAt || new Date().toISOString();
|
|
949
|
+
delete found.doc.task.column;
|
|
950
|
+
delete found.doc.task.position;
|
|
951
|
+
(0, core_3.writeTaskFile)(logPath, found.doc.task, found.doc.body);
|
|
952
|
+
fs.unlinkSync(found.filePath);
|
|
953
|
+
return { content: [{ type: 'text', text: `Task ${task} archived to logs/` }] };
|
|
954
|
+
}
|
|
955
|
+
// V1: use board
|
|
566
956
|
const result = readBoard(filePath);
|
|
567
957
|
if ('error' in result) {
|
|
568
958
|
return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
|
|
@@ -736,6 +1126,34 @@ async function mcpCommand(options) {
|
|
|
736
1126
|
}
|
|
737
1127
|
}, async ({ file, task, column }) => {
|
|
738
1128
|
const filePath = file || defaultFile;
|
|
1129
|
+
// V2: restore means move from logs/ to board/
|
|
1130
|
+
if ((0, v2_detect_1.isV2)(filePath)) {
|
|
1131
|
+
const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
|
|
1132
|
+
const board = (0, v2_detect_1.readV2BoardConfig)(filePath);
|
|
1133
|
+
let targetColumn = board.columns.find(c => c.id === column);
|
|
1134
|
+
if (!targetColumn)
|
|
1135
|
+
targetColumn = board.columns.find(c => c.title.toLowerCase() === column.toLowerCase());
|
|
1136
|
+
if (!targetColumn) {
|
|
1137
|
+
return { content: [{ type: 'text', text: `Error: Column not found: ${column}` }], isError: true };
|
|
1138
|
+
}
|
|
1139
|
+
// Look in logs
|
|
1140
|
+
const logPath = path.join(dirs.logsDir, (0, core_3.taskFileName)(task));
|
|
1141
|
+
const doc = (0, core_3.readTaskFile)(logPath);
|
|
1142
|
+
if (!doc) {
|
|
1143
|
+
return { content: [{ type: 'text', text: `Error: Task not found in logs: ${task}` }], isError: true };
|
|
1144
|
+
}
|
|
1145
|
+
// Move to board/ with column info
|
|
1146
|
+
const targetTasks = (0, core_3.readTasksDir)(dirs.boardDir).filter(t => t.task.column === targetColumn.id);
|
|
1147
|
+
doc.task.column = targetColumn.id;
|
|
1148
|
+
doc.task.position = targetTasks.length;
|
|
1149
|
+
delete doc.task.completedAt;
|
|
1150
|
+
doc.task.updatedAt = new Date().toISOString();
|
|
1151
|
+
const taskPath = path.join(dirs.boardDir, (0, core_3.taskFileName)(task));
|
|
1152
|
+
(0, core_3.writeTaskFile)(taskPath, doc.task, doc.body);
|
|
1153
|
+
fs.unlinkSync(logPath);
|
|
1154
|
+
return { content: [{ type: 'text', text: `Task ${task} restored to "${targetColumn.title}"` }] };
|
|
1155
|
+
}
|
|
1156
|
+
// V1: use board
|
|
739
1157
|
const result = readBoard(filePath);
|
|
740
1158
|
if ('error' in result) {
|
|
741
1159
|
return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
|
|
@@ -777,6 +1195,26 @@ async function mcpCommand(options) {
|
|
|
777
1195
|
}
|
|
778
1196
|
}, async ({ file, task, title }) => {
|
|
779
1197
|
const filePath = file || defaultFile;
|
|
1198
|
+
// V2: update task file directly
|
|
1199
|
+
if ((0, v2_detect_1.isV2)(filePath)) {
|
|
1200
|
+
const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
|
|
1201
|
+
const found = (0, v2_detect_1.findV2Task)(dirs, task);
|
|
1202
|
+
if (!found) {
|
|
1203
|
+
return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
|
|
1204
|
+
}
|
|
1205
|
+
const t = found.doc.task;
|
|
1206
|
+
if (!t.subtasks)
|
|
1207
|
+
t.subtasks = [];
|
|
1208
|
+
const nextId = t.subtasks.length > 0
|
|
1209
|
+
? `${task}-${Math.max(...t.subtasks.map(s => parseInt(s.id.split('-').pop() || '0', 10))) + 1}`
|
|
1210
|
+
: `${task}-1`;
|
|
1211
|
+
const newSubtask = { id: nextId, title, completed: false };
|
|
1212
|
+
t.subtasks.push(newSubtask);
|
|
1213
|
+
t.updatedAt = new Date().toISOString();
|
|
1214
|
+
(0, core_3.writeTaskFile)(found.filePath, t, found.doc.body);
|
|
1215
|
+
return { content: [{ type: 'text', text: `Subtask added: ${newSubtask.id} - ${newSubtask.title}` }] };
|
|
1216
|
+
}
|
|
1217
|
+
// V1: use board
|
|
780
1218
|
const result = readBoard(filePath);
|
|
781
1219
|
if ('error' in result) {
|
|
782
1220
|
return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
|
|
@@ -805,6 +1243,23 @@ async function mcpCommand(options) {
|
|
|
805
1243
|
}
|
|
806
1244
|
}, async ({ file, task, subtask }) => {
|
|
807
1245
|
const filePath = file || defaultFile;
|
|
1246
|
+
// V2: update task file directly
|
|
1247
|
+
if ((0, v2_detect_1.isV2)(filePath)) {
|
|
1248
|
+
const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
|
|
1249
|
+
const found = (0, v2_detect_1.findV2Task)(dirs, task);
|
|
1250
|
+
if (!found) {
|
|
1251
|
+
return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
|
|
1252
|
+
}
|
|
1253
|
+
const t = found.doc.task;
|
|
1254
|
+
if (!t.subtasks || !t.subtasks.some(s => s.id === subtask)) {
|
|
1255
|
+
return { content: [{ type: 'text', text: `Error: Subtask not found: ${subtask}` }], isError: true };
|
|
1256
|
+
}
|
|
1257
|
+
t.subtasks = t.subtasks.filter(s => s.id !== subtask);
|
|
1258
|
+
t.updatedAt = new Date().toISOString();
|
|
1259
|
+
(0, core_3.writeTaskFile)(found.filePath, t, found.doc.body);
|
|
1260
|
+
return { content: [{ type: 'text', text: `Subtask ${subtask} deleted successfully` }] };
|
|
1261
|
+
}
|
|
1262
|
+
// V1: use board
|
|
808
1263
|
const result = readBoard(filePath);
|
|
809
1264
|
if ('error' in result) {
|
|
810
1265
|
return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
|
|
@@ -830,6 +1285,26 @@ async function mcpCommand(options) {
|
|
|
830
1285
|
}
|
|
831
1286
|
}, async ({ file, task, subtask }) => {
|
|
832
1287
|
const filePath = file || defaultFile;
|
|
1288
|
+
// V2: update task file directly
|
|
1289
|
+
if ((0, v2_detect_1.isV2)(filePath)) {
|
|
1290
|
+
const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
|
|
1291
|
+
const found = (0, v2_detect_1.findV2Task)(dirs, task);
|
|
1292
|
+
if (!found) {
|
|
1293
|
+
return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
|
|
1294
|
+
}
|
|
1295
|
+
const t = found.doc.task;
|
|
1296
|
+
const st = t.subtasks?.find(s => s.id === subtask);
|
|
1297
|
+
if (!st) {
|
|
1298
|
+
return { content: [{ type: 'text', text: `Error: Subtask not found: ${subtask}` }], isError: true };
|
|
1299
|
+
}
|
|
1300
|
+
const wasCompleted = st.completed;
|
|
1301
|
+
st.completed = !st.completed;
|
|
1302
|
+
t.updatedAt = new Date().toISOString();
|
|
1303
|
+
(0, core_3.writeTaskFile)(found.filePath, t, found.doc.body);
|
|
1304
|
+
const newStatus = wasCompleted ? 'incomplete' : 'completed';
|
|
1305
|
+
return { content: [{ type: 'text', text: `Subtask ${subtask} marked as ${newStatus}` }] };
|
|
1306
|
+
}
|
|
1307
|
+
// V1: use board
|
|
833
1308
|
const result = readBoard(filePath);
|
|
834
1309
|
if ('error' in result) {
|
|
835
1310
|
return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
|
|
@@ -861,6 +1336,24 @@ async function mcpCommand(options) {
|
|
|
861
1336
|
}
|
|
862
1337
|
}, async ({ file, task, subtask, title }) => {
|
|
863
1338
|
const filePath = file || defaultFile;
|
|
1339
|
+
// V2: update task file directly
|
|
1340
|
+
if ((0, v2_detect_1.isV2)(filePath)) {
|
|
1341
|
+
const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
|
|
1342
|
+
const found = (0, v2_detect_1.findV2Task)(dirs, task);
|
|
1343
|
+
if (!found) {
|
|
1344
|
+
return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
|
|
1345
|
+
}
|
|
1346
|
+
const t = found.doc.task;
|
|
1347
|
+
const st = t.subtasks?.find(s => s.id === subtask);
|
|
1348
|
+
if (!st) {
|
|
1349
|
+
return { content: [{ type: 'text', text: `Error: Subtask not found: ${subtask}` }], isError: true };
|
|
1350
|
+
}
|
|
1351
|
+
st.title = title;
|
|
1352
|
+
t.updatedAt = new Date().toISOString();
|
|
1353
|
+
(0, core_3.writeTaskFile)(found.filePath, t, found.doc.body);
|
|
1354
|
+
return { content: [{ type: 'text', text: `Subtask ${subtask} updated to "${title}"` }] };
|
|
1355
|
+
}
|
|
1356
|
+
// V1: use board
|
|
864
1357
|
const result = readBoard(filePath);
|
|
865
1358
|
if ('error' in result) {
|
|
866
1359
|
return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
|
|
@@ -887,6 +1380,29 @@ async function mcpCommand(options) {
|
|
|
887
1380
|
}
|
|
888
1381
|
}, async ({ file, task, subtasks, completed }) => {
|
|
889
1382
|
const filePath = file || defaultFile;
|
|
1383
|
+
// V2: update task file directly
|
|
1384
|
+
if ((0, v2_detect_1.isV2)(filePath)) {
|
|
1385
|
+
const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
|
|
1386
|
+
const found = (0, v2_detect_1.findV2Task)(dirs, task);
|
|
1387
|
+
if (!found) {
|
|
1388
|
+
return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
|
|
1389
|
+
}
|
|
1390
|
+
const t = found.doc.task;
|
|
1391
|
+
if (!t.subtasks) {
|
|
1392
|
+
return { content: [{ type: 'text', text: `Error: Task has no subtasks` }], isError: true };
|
|
1393
|
+
}
|
|
1394
|
+
const subtaskSet = new Set(subtasks);
|
|
1395
|
+
for (const st of t.subtasks) {
|
|
1396
|
+
if (subtaskSet.has(st.id)) {
|
|
1397
|
+
st.completed = completed;
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
t.updatedAt = new Date().toISOString();
|
|
1401
|
+
(0, core_3.writeTaskFile)(found.filePath, t, found.doc.body);
|
|
1402
|
+
const status = completed ? 'completed' : 'incomplete';
|
|
1403
|
+
return { content: [{ type: 'text', text: `${subtasks.length} subtasks marked as ${status}` }] };
|
|
1404
|
+
}
|
|
1405
|
+
// V1: use board
|
|
890
1406
|
const result = readBoard(filePath);
|
|
891
1407
|
if ('error' in result) {
|
|
892
1408
|
return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
|
|
@@ -913,13 +1429,32 @@ async function mcpCommand(options) {
|
|
|
913
1429
|
}
|
|
914
1430
|
}, async ({ file, task, completed }) => {
|
|
915
1431
|
const filePath = file || defaultFile;
|
|
1432
|
+
const markCompleted = completed ?? true;
|
|
1433
|
+
// V2: update task file directly
|
|
1434
|
+
if ((0, v2_detect_1.isV2)(filePath)) {
|
|
1435
|
+
const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
|
|
1436
|
+
const found = (0, v2_detect_1.findV2Task)(dirs, task);
|
|
1437
|
+
if (!found) {
|
|
1438
|
+
return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
|
|
1439
|
+
}
|
|
1440
|
+
const t = found.doc.task;
|
|
1441
|
+
const count = t.subtasks?.length || 0;
|
|
1442
|
+
if (t.subtasks) {
|
|
1443
|
+
for (const st of t.subtasks) {
|
|
1444
|
+
st.completed = markCompleted;
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
t.updatedAt = new Date().toISOString();
|
|
1448
|
+
(0, core_3.writeTaskFile)(found.filePath, t, found.doc.body);
|
|
1449
|
+
const status = markCompleted ? 'completed' : 'incomplete';
|
|
1450
|
+
return { content: [{ type: 'text', text: `All ${count} subtasks in ${task} marked as ${status}` }] };
|
|
1451
|
+
}
|
|
1452
|
+
// V1: use board
|
|
916
1453
|
const result = readBoard(filePath);
|
|
917
1454
|
if ('error' in result) {
|
|
918
1455
|
return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
|
|
919
1456
|
}
|
|
920
1457
|
let { board } = result;
|
|
921
|
-
// Default to true if not specified
|
|
922
|
-
const markCompleted = completed ?? true;
|
|
923
1458
|
const bulkResult = (0, core_1.setAllSubtasksCompleted)(board, task, markCompleted);
|
|
924
1459
|
if (!bulkResult.success) {
|
|
925
1460
|
return { content: [{ type: 'text', text: `Error: ${bulkResult.error}` }], isError: true };
|
|
@@ -947,6 +1482,37 @@ async function mcpCommand(options) {
|
|
|
947
1482
|
}
|
|
948
1483
|
}, async ({ file, tasks, column }) => {
|
|
949
1484
|
const filePath = file || defaultFile;
|
|
1485
|
+
// V2: update each task file
|
|
1486
|
+
if ((0, v2_detect_1.isV2)(filePath)) {
|
|
1487
|
+
const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
|
|
1488
|
+
const board = (0, v2_detect_1.readV2BoardConfig)(filePath);
|
|
1489
|
+
let targetColumn = board.columns.find(c => c.id === column);
|
|
1490
|
+
if (!targetColumn)
|
|
1491
|
+
targetColumn = board.columns.find(c => c.title.toLowerCase() === column.toLowerCase());
|
|
1492
|
+
if (!targetColumn) {
|
|
1493
|
+
return { content: [{ type: 'text', text: `Error: Column not found: ${column}` }], isError: true };
|
|
1494
|
+
}
|
|
1495
|
+
const results = [];
|
|
1496
|
+
let successCount = 0;
|
|
1497
|
+
let failureCount = 0;
|
|
1498
|
+
for (const taskId of tasks) {
|
|
1499
|
+
const taskPath = path.join(dirs.boardDir, (0, core_3.taskFileName)(taskId));
|
|
1500
|
+
const doc = (0, core_3.readTaskFile)(taskPath);
|
|
1501
|
+
if (!doc) {
|
|
1502
|
+
results.push({ taskId, success: false, error: 'Task not found' });
|
|
1503
|
+
failureCount++;
|
|
1504
|
+
continue;
|
|
1505
|
+
}
|
|
1506
|
+
doc.task.column = targetColumn.id;
|
|
1507
|
+
doc.task.updatedAt = new Date().toISOString();
|
|
1508
|
+
(0, core_3.writeTaskFile)(taskPath, doc.task, doc.body);
|
|
1509
|
+
results.push({ taskId, success: true });
|
|
1510
|
+
successCount++;
|
|
1511
|
+
}
|
|
1512
|
+
const output = { success: failureCount === 0, successCount, failureCount, results };
|
|
1513
|
+
return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }], isError: failureCount > 0 && successCount === 0 };
|
|
1514
|
+
}
|
|
1515
|
+
// V1: use board
|
|
950
1516
|
const result = readBoard(filePath);
|
|
951
1517
|
if ('error' in result) {
|
|
952
1518
|
return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
|
|
@@ -1008,13 +1574,54 @@ async function mcpCommand(options) {
|
|
|
1008
1574
|
}
|
|
1009
1575
|
}, async ({ file, tasks, priority, tags, assignee }) => {
|
|
1010
1576
|
const filePath = file || defaultFile;
|
|
1577
|
+
const isNull = (v) => v === null || v === 'null';
|
|
1578
|
+
// V2: update each task file
|
|
1579
|
+
if ((0, v2_detect_1.isV2)(filePath)) {
|
|
1580
|
+
const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
|
|
1581
|
+
const results = [];
|
|
1582
|
+
let successCount = 0;
|
|
1583
|
+
let failureCount = 0;
|
|
1584
|
+
for (const taskId of tasks) {
|
|
1585
|
+
const taskPath = path.join(dirs.boardDir, (0, core_3.taskFileName)(taskId));
|
|
1586
|
+
const doc = (0, core_3.readTaskFile)(taskPath);
|
|
1587
|
+
if (!doc) {
|
|
1588
|
+
results.push({ taskId, success: false, error: 'Task not found' });
|
|
1589
|
+
failureCount++;
|
|
1590
|
+
continue;
|
|
1591
|
+
}
|
|
1592
|
+
const t = doc.task;
|
|
1593
|
+
if (priority !== undefined) {
|
|
1594
|
+
if (isNull(priority))
|
|
1595
|
+
delete t.priority;
|
|
1596
|
+
else
|
|
1597
|
+
t.priority = priority;
|
|
1598
|
+
}
|
|
1599
|
+
if (tags !== undefined) {
|
|
1600
|
+
if (isNull(tags))
|
|
1601
|
+
delete t.tags;
|
|
1602
|
+
else
|
|
1603
|
+
t.tags = tags;
|
|
1604
|
+
}
|
|
1605
|
+
if (assignee !== undefined) {
|
|
1606
|
+
if (isNull(assignee))
|
|
1607
|
+
delete t.assignee;
|
|
1608
|
+
else
|
|
1609
|
+
t.assignee = assignee;
|
|
1610
|
+
}
|
|
1611
|
+
t.updatedAt = new Date().toISOString();
|
|
1612
|
+
(0, core_3.writeTaskFile)(taskPath, t, doc.body);
|
|
1613
|
+
results.push({ taskId, success: true });
|
|
1614
|
+
successCount++;
|
|
1615
|
+
}
|
|
1616
|
+
const output = { success: failureCount === 0, successCount, failureCount, results };
|
|
1617
|
+
return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }], isError: failureCount > 0 && successCount === 0 };
|
|
1618
|
+
}
|
|
1619
|
+
// V1: use board
|
|
1011
1620
|
const result = readBoard(filePath);
|
|
1012
1621
|
if ('error' in result) {
|
|
1013
1622
|
return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
|
|
1014
1623
|
}
|
|
1015
1624
|
let { board } = result;
|
|
1016
|
-
// Helper to check for null or "null" string
|
|
1017
|
-
const isNull = (v) => v === null || v === 'null';
|
|
1018
1625
|
const patch = {};
|
|
1019
1626
|
if (priority !== undefined)
|
|
1020
1627
|
patch.priority = isNull(priority) ? undefined : priority;
|
|
@@ -1047,6 +1654,33 @@ async function mcpCommand(options) {
|
|
|
1047
1654
|
}
|
|
1048
1655
|
}, async ({ file, tasks }) => {
|
|
1049
1656
|
const filePath = file || defaultFile;
|
|
1657
|
+
// V2: delete task files
|
|
1658
|
+
if ((0, v2_detect_1.isV2)(filePath)) {
|
|
1659
|
+
const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
|
|
1660
|
+
const results = [];
|
|
1661
|
+
let successCount = 0;
|
|
1662
|
+
let failureCount = 0;
|
|
1663
|
+
for (const taskId of tasks) {
|
|
1664
|
+
const found = (0, v2_detect_1.findV2Task)(dirs, taskId, true);
|
|
1665
|
+
if (!found) {
|
|
1666
|
+
results.push({ taskId, success: false, error: 'Task not found' });
|
|
1667
|
+
failureCount++;
|
|
1668
|
+
continue;
|
|
1669
|
+
}
|
|
1670
|
+
try {
|
|
1671
|
+
fs.unlinkSync(found.filePath);
|
|
1672
|
+
results.push({ taskId, success: true });
|
|
1673
|
+
successCount++;
|
|
1674
|
+
}
|
|
1675
|
+
catch (e) {
|
|
1676
|
+
results.push({ taskId, success: false, error: e.message });
|
|
1677
|
+
failureCount++;
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
const output = { success: failureCount === 0, successCount, failureCount, results };
|
|
1681
|
+
return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }], isError: failureCount > 0 && successCount === 0 };
|
|
1682
|
+
}
|
|
1683
|
+
// V1: use board
|
|
1050
1684
|
const result = readBoard(filePath);
|
|
1051
1685
|
if ('error' in result) {
|
|
1052
1686
|
return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
|
|
@@ -1077,6 +1711,39 @@ async function mcpCommand(options) {
|
|
|
1077
1711
|
}
|
|
1078
1712
|
}, async ({ file, tasks }) => {
|
|
1079
1713
|
const filePath = file || defaultFile;
|
|
1714
|
+
// V2: move task files from board/ to logs/
|
|
1715
|
+
if ((0, v2_detect_1.isV2)(filePath)) {
|
|
1716
|
+
const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
|
|
1717
|
+
const results = [];
|
|
1718
|
+
let successCount = 0;
|
|
1719
|
+
let failureCount = 0;
|
|
1720
|
+
for (const taskId of tasks) {
|
|
1721
|
+
const taskPath = path.join(dirs.boardDir, (0, core_3.taskFileName)(taskId));
|
|
1722
|
+
const doc = (0, core_3.readTaskFile)(taskPath);
|
|
1723
|
+
if (!doc) {
|
|
1724
|
+
results.push({ taskId, success: false, error: 'Task not found' });
|
|
1725
|
+
failureCount++;
|
|
1726
|
+
continue;
|
|
1727
|
+
}
|
|
1728
|
+
try {
|
|
1729
|
+
const logPath = path.join(dirs.logsDir, (0, core_3.taskFileName)(taskId));
|
|
1730
|
+
doc.task.completedAt = doc.task.completedAt || new Date().toISOString();
|
|
1731
|
+
delete doc.task.column;
|
|
1732
|
+
delete doc.task.position;
|
|
1733
|
+
(0, core_3.writeTaskFile)(logPath, doc.task, doc.body);
|
|
1734
|
+
fs.unlinkSync(taskPath);
|
|
1735
|
+
results.push({ taskId, success: true });
|
|
1736
|
+
successCount++;
|
|
1737
|
+
}
|
|
1738
|
+
catch (e) {
|
|
1739
|
+
results.push({ taskId, success: false, error: e.message });
|
|
1740
|
+
failureCount++;
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
const output = { success: failureCount === 0, successCount, failureCount, results };
|
|
1744
|
+
return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }], isError: failureCount > 0 && successCount === 0 };
|
|
1745
|
+
}
|
|
1746
|
+
// V1: use board
|
|
1080
1747
|
const results = [];
|
|
1081
1748
|
let successCount = 0;
|
|
1082
1749
|
let failureCount = 0;
|
|
@@ -1175,6 +1842,274 @@ async function mcpCommand(options) {
|
|
|
1175
1842
|
isError: !result.ok
|
|
1176
1843
|
};
|
|
1177
1844
|
});
|
|
1845
|
+
// ==========================================================================
|
|
1846
|
+
// TYPES
|
|
1847
|
+
// ==========================================================================
|
|
1848
|
+
server.registerTool('list_types', {
|
|
1849
|
+
title: 'List Types',
|
|
1850
|
+
description: 'List board strict mode and custom type configuration',
|
|
1851
|
+
inputSchema: {
|
|
1852
|
+
file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
|
|
1853
|
+
}
|
|
1854
|
+
}, async ({ file }) => {
|
|
1855
|
+
const filePath = file || defaultFile;
|
|
1856
|
+
if ((0, v2_detect_1.isV2)(filePath)) {
|
|
1857
|
+
try {
|
|
1858
|
+
const board = (0, v2_detect_1.readV2BoardConfig)(filePath);
|
|
1859
|
+
const boardConfig = board;
|
|
1860
|
+
const output = {
|
|
1861
|
+
strict: boardConfig.strict === true,
|
|
1862
|
+
types: sanitizeTypesConfig(boardConfig.types),
|
|
1863
|
+
};
|
|
1864
|
+
return {
|
|
1865
|
+
content: [{ type: 'text', text: JSON.stringify(output, null, 2) }]
|
|
1866
|
+
};
|
|
1867
|
+
}
|
|
1868
|
+
catch (e) {
|
|
1869
|
+
return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
const result = readBoard(filePath);
|
|
1873
|
+
if ('error' in result) {
|
|
1874
|
+
return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
|
|
1875
|
+
}
|
|
1876
|
+
const boardConfig = result.board;
|
|
1877
|
+
const strict = boardConfig.strict === true;
|
|
1878
|
+
const types = sanitizeTypesConfig(boardConfig.types);
|
|
1879
|
+
return {
|
|
1880
|
+
content: [{
|
|
1881
|
+
type: 'text',
|
|
1882
|
+
text: JSON.stringify({ strict, types }, null, 2)
|
|
1883
|
+
}]
|
|
1884
|
+
};
|
|
1885
|
+
});
|
|
1886
|
+
// ==========================================================================
|
|
1887
|
+
// RULES
|
|
1888
|
+
// ==========================================================================
|
|
1889
|
+
// List rules tool
|
|
1890
|
+
server.registerTool('list_rules', {
|
|
1891
|
+
title: 'List Rules',
|
|
1892
|
+
description: 'List all project rules (always, never, prefer, context) from the brainfile',
|
|
1893
|
+
inputSchema: {
|
|
1894
|
+
file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
|
|
1895
|
+
category: zod_1.z.enum(['always', 'never', 'prefer', 'context']).optional().describe('Filter by rule category')
|
|
1896
|
+
}
|
|
1897
|
+
}, async ({ file, category }) => {
|
|
1898
|
+
const filePath = file || defaultFile;
|
|
1899
|
+
const result = readBoard(filePath);
|
|
1900
|
+
if ('error' in result) {
|
|
1901
|
+
return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
|
|
1902
|
+
}
|
|
1903
|
+
const { board } = result;
|
|
1904
|
+
const rules = board.rules || {};
|
|
1905
|
+
// Filter by category if specified
|
|
1906
|
+
let outputRules = rules;
|
|
1907
|
+
if (category) {
|
|
1908
|
+
outputRules = { [category]: rules[category] || [] };
|
|
1909
|
+
}
|
|
1910
|
+
// Count total rules
|
|
1911
|
+
const countRules = (r) => (r.always?.length || 0) +
|
|
1912
|
+
(r.never?.length || 0) +
|
|
1913
|
+
(r.prefer?.length || 0) +
|
|
1914
|
+
(r.context?.length || 0);
|
|
1915
|
+
const output = {
|
|
1916
|
+
rules: outputRules,
|
|
1917
|
+
totalCount: category
|
|
1918
|
+
? (outputRules[category]?.length || 0)
|
|
1919
|
+
: countRules(rules),
|
|
1920
|
+
};
|
|
1921
|
+
return {
|
|
1922
|
+
content: [{ type: 'text', text: JSON.stringify(output, null, 2) }]
|
|
1923
|
+
};
|
|
1924
|
+
});
|
|
1925
|
+
// Add rule tool
|
|
1926
|
+
server.registerTool('add_rule', {
|
|
1927
|
+
title: 'Add Rule',
|
|
1928
|
+
description: 'Add a new project rule to the brainfile',
|
|
1929
|
+
inputSchema: {
|
|
1930
|
+
file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
|
|
1931
|
+
category: zod_1.z.enum(['always', 'never', 'prefer', 'context']).describe('Rule category'),
|
|
1932
|
+
text: zod_1.z.string().describe('Rule text/description')
|
|
1933
|
+
}
|
|
1934
|
+
}, async ({ file, category, text }) => {
|
|
1935
|
+
const filePath = file || defaultFile;
|
|
1936
|
+
const result = readBoard(filePath);
|
|
1937
|
+
if ('error' in result) {
|
|
1938
|
+
return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
|
|
1939
|
+
}
|
|
1940
|
+
let { board } = result;
|
|
1941
|
+
const addResult = (0, core_1.addRule)(board, category, text);
|
|
1942
|
+
if (!addResult.success || !addResult.board) {
|
|
1943
|
+
return { content: [{ type: 'text', text: `Error: ${addResult.error}` }], isError: true };
|
|
1944
|
+
}
|
|
1945
|
+
writeBoard(filePath, addResult.board);
|
|
1946
|
+
// Find the newly added rule (last one in the category)
|
|
1947
|
+
const newRules = addResult.board.rules?.[category] || [];
|
|
1948
|
+
const newRule = newRules[newRules.length - 1];
|
|
1949
|
+
const output = {
|
|
1950
|
+
success: true,
|
|
1951
|
+
category,
|
|
1952
|
+
rule: newRule,
|
|
1953
|
+
};
|
|
1954
|
+
return {
|
|
1955
|
+
content: [{ type: 'text', text: JSON.stringify(output, null, 2) }]
|
|
1956
|
+
};
|
|
1957
|
+
});
|
|
1958
|
+
// Delete rule tool
|
|
1959
|
+
server.registerTool('delete_rule', {
|
|
1960
|
+
title: 'Delete Rule',
|
|
1961
|
+
description: 'Delete a project rule from the brainfile by category and ID',
|
|
1962
|
+
inputSchema: {
|
|
1963
|
+
file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
|
|
1964
|
+
category: zod_1.z.enum(['always', 'never', 'prefer', 'context']).describe('Rule category'),
|
|
1965
|
+
id: zod_1.z.number().describe('Rule ID to delete')
|
|
1966
|
+
}
|
|
1967
|
+
}, async ({ file, category, id }) => {
|
|
1968
|
+
const filePath = file || defaultFile;
|
|
1969
|
+
const result = readBoard(filePath);
|
|
1970
|
+
if ('error' in result) {
|
|
1971
|
+
return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
|
|
1972
|
+
}
|
|
1973
|
+
let { board } = result;
|
|
1974
|
+
// Find the rule being deleted for the response
|
|
1975
|
+
const existingRules = board.rules?.[category] || [];
|
|
1976
|
+
const ruleToDelete = existingRules.find((r) => r.id === id);
|
|
1977
|
+
if (!ruleToDelete) {
|
|
1978
|
+
const availableIds = existingRules.map((r) => r.id).join(', ');
|
|
1979
|
+
return {
|
|
1980
|
+
content: [{
|
|
1981
|
+
type: 'text',
|
|
1982
|
+
text: `Error: Rule ${id} not found in ${category}. ${availableIds ? `Available IDs: ${availableIds}` : `No rules in ${category} category`}`
|
|
1983
|
+
}],
|
|
1984
|
+
isError: true
|
|
1985
|
+
};
|
|
1986
|
+
}
|
|
1987
|
+
const deleteResult = (0, core_1.deleteRule)(board, category, id);
|
|
1988
|
+
if (!deleteResult.success || !deleteResult.board) {
|
|
1989
|
+
return { content: [{ type: 'text', text: `Error: ${deleteResult.error}` }], isError: true };
|
|
1990
|
+
}
|
|
1991
|
+
writeBoard(filePath, deleteResult.board);
|
|
1992
|
+
const output = {
|
|
1993
|
+
success: true,
|
|
1994
|
+
category,
|
|
1995
|
+
deletedRule: ruleToDelete,
|
|
1996
|
+
};
|
|
1997
|
+
return {
|
|
1998
|
+
content: [{ type: 'text', text: JSON.stringify(output, null, 2) }]
|
|
1999
|
+
};
|
|
2000
|
+
});
|
|
2001
|
+
// ==========================================================================
|
|
2002
|
+
// V2 NEW TOOLS: complete_task, search_logs, append_log
|
|
2003
|
+
// ==========================================================================
|
|
2004
|
+
// Complete task tool
|
|
2005
|
+
server.registerTool('complete_task', {
|
|
2006
|
+
title: 'Complete Task',
|
|
2007
|
+
description: 'Complete a task - in v2, moves task file from board/ to logs/ with completedAt timestamp. In v1, moves to done column.',
|
|
2008
|
+
inputSchema: {
|
|
2009
|
+
file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
|
|
2010
|
+
task: zod_1.z.string().describe('Task ID to complete'),
|
|
2011
|
+
}
|
|
2012
|
+
}, async ({ file, task }) => {
|
|
2013
|
+
const filePath = file || defaultFile;
|
|
2014
|
+
try {
|
|
2015
|
+
const { completeCommand } = await Promise.resolve().then(() => __importStar(require('./complete')));
|
|
2016
|
+
const result = completeCommand({ file: filePath, task }, { log: () => { }, warn: () => { }, error: () => { }, info: () => { } });
|
|
2017
|
+
return {
|
|
2018
|
+
content: [{ type: 'text', text: `Task ${task} completed at ${result.completedAt}` }]
|
|
2019
|
+
};
|
|
2020
|
+
}
|
|
2021
|
+
catch (e) {
|
|
2022
|
+
return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
|
|
2023
|
+
}
|
|
2024
|
+
});
|
|
2025
|
+
// Search logs tool
|
|
2026
|
+
server.registerTool('search_logs', {
|
|
2027
|
+
title: 'Search Logs',
|
|
2028
|
+
description: 'Search across completed task logs. Requires v2 per-task file architecture.',
|
|
2029
|
+
inputSchema: {
|
|
2030
|
+
file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
|
|
2031
|
+
query: zod_1.z.string().optional().describe('Search query to match against log content'),
|
|
2032
|
+
recent: zod_1.z.boolean().optional().describe('List recently completed tasks'),
|
|
2033
|
+
task: zod_1.z.string().optional().describe('View a specific task log'),
|
|
2034
|
+
}
|
|
2035
|
+
}, async ({ file, query, recent, task: taskId }) => {
|
|
2036
|
+
const filePath = file || defaultFile;
|
|
2037
|
+
if (!(0, v2_detect_1.isV2)(filePath)) {
|
|
2038
|
+
return { content: [{ type: 'text', text: 'Error: search_logs requires v2 per-task file architecture. Run: brainfile migrate --v2' }], isError: true };
|
|
2039
|
+
}
|
|
2040
|
+
const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
|
|
2041
|
+
// View specific task log
|
|
2042
|
+
if (taskId) {
|
|
2043
|
+
const found = (0, v2_detect_1.findV2Task)(dirs, taskId, true);
|
|
2044
|
+
if (!found) {
|
|
2045
|
+
return { content: [{ type: 'text', text: `Error: Task not found: ${taskId}` }], isError: true };
|
|
2046
|
+
}
|
|
2047
|
+
const output = {
|
|
2048
|
+
id: found.doc.task.id,
|
|
2049
|
+
title: found.doc.task.title,
|
|
2050
|
+
completedAt: found.doc.task.completedAt,
|
|
2051
|
+
description: (0, v2_detect_1.extractDescription)(found.doc.body),
|
|
2052
|
+
log: (0, v2_detect_1.extractLog)(found.doc.body),
|
|
2053
|
+
};
|
|
2054
|
+
return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
|
|
2055
|
+
}
|
|
2056
|
+
// Search logs
|
|
2057
|
+
if (query) {
|
|
2058
|
+
const logDocs = (0, core_3.readTasksDir)(dirs.logsDir);
|
|
2059
|
+
const queryLower = query.toLowerCase();
|
|
2060
|
+
const matches = [];
|
|
2061
|
+
for (const doc of logDocs) {
|
|
2062
|
+
const task = doc.task;
|
|
2063
|
+
const desc = (0, v2_detect_1.extractDescription)(doc.body) || '';
|
|
2064
|
+
const log = (0, v2_detect_1.extractLog)(doc.body) || '';
|
|
2065
|
+
const fullText = [task.title, task.description || '', desc, log].join(' ').toLowerCase();
|
|
2066
|
+
if (fullText.includes(queryLower)) {
|
|
2067
|
+
matches.push({ id: task.id, title: task.title, completedAt: task.completedAt });
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
return { content: [{ type: 'text', text: JSON.stringify({ results: matches, count: matches.length }, null, 2) }] };
|
|
2071
|
+
}
|
|
2072
|
+
// Recent logs (default)
|
|
2073
|
+
const logDocs = (0, core_3.readTasksDir)(dirs.logsDir);
|
|
2074
|
+
logDocs.sort((a, b) => (b.task.completedAt || '').localeCompare(a.task.completedAt || ''));
|
|
2075
|
+
const recent20 = logDocs.slice(0, 20).map(doc => ({
|
|
2076
|
+
id: doc.task.id,
|
|
2077
|
+
title: doc.task.title,
|
|
2078
|
+
completedAt: doc.task.completedAt,
|
|
2079
|
+
}));
|
|
2080
|
+
return { content: [{ type: 'text', text: JSON.stringify({ logs: recent20, count: recent20.length }, null, 2) }] };
|
|
2081
|
+
});
|
|
2082
|
+
// Append log tool
|
|
2083
|
+
server.registerTool('append_log', {
|
|
2084
|
+
title: 'Append Log',
|
|
2085
|
+
description: 'Append a timestamped entry to a task log section. Works on both active tasks and completed logs. Requires v2 per-task file architecture.',
|
|
2086
|
+
inputSchema: {
|
|
2087
|
+
file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
|
|
2088
|
+
task: zod_1.z.string().describe('Task ID to append log to'),
|
|
2089
|
+
message: zod_1.z.string().describe('Log message to append'),
|
|
2090
|
+
agent: zod_1.z.string().optional().describe('Agent name for attribution'),
|
|
2091
|
+
}
|
|
2092
|
+
}, async ({ file, task: taskId, message, agent }) => {
|
|
2093
|
+
const filePath = file || defaultFile;
|
|
2094
|
+
if (!(0, v2_detect_1.isV2)(filePath)) {
|
|
2095
|
+
return { content: [{ type: 'text', text: 'Error: append_log requires v2 per-task file architecture. Run: brainfile migrate --v2' }], isError: true };
|
|
2096
|
+
}
|
|
2097
|
+
const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
|
|
2098
|
+
const found = (0, v2_detect_1.findV2Task)(dirs, taskId, true);
|
|
2099
|
+
if (!found) {
|
|
2100
|
+
return { content: [{ type: 'text', text: `Error: Task not found: ${taskId}` }], isError: true };
|
|
2101
|
+
}
|
|
2102
|
+
const { doc, filePath: taskFilePath } = found;
|
|
2103
|
+
const timestamp = new Date().toISOString();
|
|
2104
|
+
const agentPrefix = agent ? `[${agent}] ` : '';
|
|
2105
|
+
const entry = `- ${timestamp}: ${agentPrefix}${message}`;
|
|
2106
|
+
const existingDescription = (0, v2_detect_1.extractDescription)(doc.body);
|
|
2107
|
+
const existingLog = (0, v2_detect_1.extractLog)(doc.body) || '';
|
|
2108
|
+
const newLog = existingLog ? `${existingLog}\n${entry}` : entry;
|
|
2109
|
+
const newBody = (0, v2_detect_1.composeBody)(existingDescription, newLog);
|
|
2110
|
+
(0, core_3.writeTaskFile)(taskFilePath, doc.task, newBody);
|
|
2111
|
+
return { content: [{ type: 'text', text: `Log entry added to ${taskId}: ${entry}` }] };
|
|
2112
|
+
});
|
|
1178
2113
|
// Connect via stdio
|
|
1179
2114
|
const transport = new stdio_js_1.StdioServerTransport();
|
|
1180
2115
|
await server.connect(transport);
|