@brainfile/cli 0.17.0 → 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.brainfile/brainfile.md.v1.bak +171 -0
- package/.brainfile/logs/task-1.md +206 -0
- package/.brainfile/logs/task-2.md +482 -0
- package/.brainfile/logs/task-3.md +314 -0
- package/.github/workflows/release.yml +35 -0
- package/README.md +3 -5
- package/dist/cli.js +29 -53
- package/dist/cli.js.map +1 -1
- package/dist/commands/add.d.ts +2 -0
- package/dist/commands/add.d.ts.map +1 -1
- package/dist/commands/add.js +16 -2
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/complete.d.ts +5 -7
- package/dist/commands/complete.d.ts.map +1 -1
- package/dist/commands/complete.js +76 -127
- package/dist/commands/complete.js.map +1 -1
- package/dist/commands/contract.d.ts +46 -2
- package/dist/commands/contract.d.ts.map +1 -1
- package/dist/commands/contract.js +499 -2
- package/dist/commands/contract.js.map +1 -1
- package/dist/commands/mcp.d.ts.map +1 -1
- package/dist/commands/mcp.js +1037 -1763
- package/dist/commands/mcp.js.map +1 -1
- package/dist/commands/migrate.d.ts +0 -2
- package/dist/commands/migrate.d.ts.map +1 -1
- package/dist/commands/migrate.js +23 -89
- package/dist/commands/migrate.js.map +1 -1
- package/dist/commands/move.js.map +1 -1
- package/dist/commands/schema.d.ts.map +1 -1
- package/dist/commands/schema.js +6 -1
- package/dist/commands/schema.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/contractRunner.d.ts.map +1 -1
- package/dist/lib/contractRunner.js +76 -49
- package/dist/lib/contractRunner.js.map +1 -1
- package/dist/mcp/tools/contract.d.ts +25 -0
- package/dist/mcp/tools/contract.d.ts.map +1 -0
- package/dist/mcp/tools/contract.js +33 -0
- package/dist/mcp/tools/contract.js.map +1 -0
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js +22 -9
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/contractSpec.d.ts +2 -0
- package/dist/utils/contractSpec.d.ts.map +1 -1
- package/dist/utils/contractSpec.js +5 -1
- package/dist/utils/contractSpec.js.map +1 -1
- package/dist/utils/hook-settings.d.ts.map +1 -1
- package/dist/utils/hook-settings.js +5 -1
- package/dist/utils/hook-settings.js.map +1 -1
- package/dist/validation/command-lint.d.ts +11 -0
- package/dist/validation/command-lint.d.ts.map +1 -0
- package/dist/validation/command-lint.js +88 -0
- package/dist/validation/command-lint.js.map +1 -0
- package/package.json +2 -2
- package/dist/commands/context.d.ts +0 -19
- package/dist/commands/context.d.ts.map +0 -1
- package/dist/commands/context.js +0 -103
- package/dist/commands/context.js.map +0 -1
- package/dist/commands/history.d.ts +0 -18
- package/dist/commands/history.d.ts.map +0 -1
- package/dist/commands/history.js +0 -92
- package/dist/commands/history.js.map +0 -1
- package/dist/commands/ledger-rebuild.d.ts +0 -17
- package/dist/commands/ledger-rebuild.d.ts.map +0 -1
- package/dist/commands/ledger-rebuild.js +0 -287
- package/dist/commands/ledger-rebuild.js.map +0 -1
- package/dist/commands/ledger.d.ts +0 -20
- package/dist/commands/ledger.d.ts.map +0 -1
- package/dist/commands/ledger.js +0 -111
- package/dist/commands/ledger.js.map +0 -1
- package/dist/commands/migrate-ledger.d.ts +0 -17
- package/dist/commands/migrate-ledger.d.ts.map +0 -1
- package/dist/commands/migrate-ledger.js +0 -127
- package/dist/commands/migrate-ledger.js.map +0 -1
- package/dist/commands/stats.d.ts +0 -20
- package/dist/commands/stats.d.ts.map +0 -1
- package/dist/commands/stats.js +0 -151
- package/dist/commands/stats.js.map +0 -1
- package/dist/tui/components/ArchivePanel.d.ts +0 -16
- package/dist/tui/components/ArchivePanel.d.ts.map +0 -1
- package/dist/tui/components/ArchivePanel.js +0 -115
- package/dist/tui/components/ArchivePanel.js.map +0 -1
- package/dist/utils/date-helpers.d.ts +0 -20
- package/dist/utils/date-helpers.d.ts.map +0 -1
- package/dist/utils/date-helpers.js +0 -54
- package/dist/utils/date-helpers.js.map +0 -1
- package/dist/utils/v2-tasks.d.ts +0 -121
- package/dist/utils/v2-tasks.d.ts.map +0 -1
- package/dist/utils/v2-tasks.js +0 -384
- package/dist/utils/v2-tasks.js.map +0 -1
package/dist/commands/mcp.js
CHANGED
|
@@ -39,7 +39,6 @@ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
|
39
39
|
const zod_1 = require("zod");
|
|
40
40
|
const fs = __importStar(require("fs"));
|
|
41
41
|
const path = __importStar(require("path"));
|
|
42
|
-
const child_process_1 = require("child_process");
|
|
43
42
|
const core_1 = require("@brainfile/core");
|
|
44
43
|
const errorHandler_1 = require("../utils/errorHandler");
|
|
45
44
|
const contractSpec_1 = require("../utils/contractSpec");
|
|
@@ -49,9 +48,9 @@ const github_auth_1 = require("../utils/github-auth");
|
|
|
49
48
|
const linear_auth_1 = require("../utils/linear-auth");
|
|
50
49
|
const core_2 = require("@brainfile/core");
|
|
51
50
|
const contractRunner_1 = require("../lib/contractRunner");
|
|
51
|
+
const contract_1 = require("../mcp/tools/contract");
|
|
52
52
|
const archive_1 = require("../utils/archive");
|
|
53
53
|
const core_3 = require("@brainfile/core");
|
|
54
|
-
const dist_1 = require("../../../core/dist");
|
|
55
54
|
const v2_detect_1 = require("../utils/v2-detect");
|
|
56
55
|
function sanitizeTypesConfig(raw) {
|
|
57
56
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
@@ -131,80 +130,6 @@ function findGitRoot(startDir) {
|
|
|
131
130
|
}
|
|
132
131
|
return null;
|
|
133
132
|
}
|
|
134
|
-
const LEDGER_CONTRACT_STATUSES = [
|
|
135
|
-
'ready',
|
|
136
|
-
'in_progress',
|
|
137
|
-
'delivered',
|
|
138
|
-
'done',
|
|
139
|
-
'failed',
|
|
140
|
-
'blocked',
|
|
141
|
-
];
|
|
142
|
-
function resolveLedgerLogsDir(filePath) {
|
|
143
|
-
if ((0, v2_detect_1.isV2)(filePath)) {
|
|
144
|
-
return (0, v2_detect_1.getV2Dirs)(filePath).logsDir;
|
|
145
|
-
}
|
|
146
|
-
const absolutePath = path.resolve(filePath);
|
|
147
|
-
const parentDir = path.dirname(absolutePath);
|
|
148
|
-
const stateDir = path.basename(parentDir) === '.brainfile'
|
|
149
|
-
? parentDir
|
|
150
|
-
: path.join(parentDir, '.brainfile');
|
|
151
|
-
return path.join(stateDir, 'logs');
|
|
152
|
-
}
|
|
153
|
-
function normalizePositiveInt(value, fallback) {
|
|
154
|
-
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
|
|
155
|
-
return fallback;
|
|
156
|
-
}
|
|
157
|
-
return Math.floor(value);
|
|
158
|
-
}
|
|
159
|
-
function normalizeNonNegativeInt(value, fallback) {
|
|
160
|
-
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
|
|
161
|
-
return fallback;
|
|
162
|
-
}
|
|
163
|
-
return Math.floor(value);
|
|
164
|
-
}
|
|
165
|
-
function toIsoDaysAgo(days) {
|
|
166
|
-
const now = Date.now();
|
|
167
|
-
const msPerDay = 24 * 60 * 60 * 1000;
|
|
168
|
-
return new Date(now - (days * msPerDay)).toISOString();
|
|
169
|
-
}
|
|
170
|
-
function inferFilesChangedFromGit(filePath) {
|
|
171
|
-
const repoRoot = findGitRoot(path.dirname(path.resolve(filePath)));
|
|
172
|
-
if (!repoRoot) {
|
|
173
|
-
return [];
|
|
174
|
-
}
|
|
175
|
-
const inferred = new Set();
|
|
176
|
-
const commands = ['git diff --name-only -- .', 'git diff --name-only --cached -- .'];
|
|
177
|
-
for (const command of commands) {
|
|
178
|
-
try {
|
|
179
|
-
const stdout = (0, child_process_1.execSync)(command, {
|
|
180
|
-
cwd: repoRoot,
|
|
181
|
-
encoding: 'utf-8',
|
|
182
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
183
|
-
});
|
|
184
|
-
for (const line of stdout.split('\n')) {
|
|
185
|
-
const trimmed = line.trim();
|
|
186
|
-
if (trimmed) {
|
|
187
|
-
inferred.add(trimmed.replace(/\\/g, '/'));
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
catch {
|
|
192
|
-
// Ignore git inference errors and fall back to core defaults.
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
return Array.from(inferred);
|
|
196
|
-
}
|
|
197
|
-
function median(values) {
|
|
198
|
-
if (values.length === 0) {
|
|
199
|
-
return 0;
|
|
200
|
-
}
|
|
201
|
-
const sorted = [...values].sort((a, b) => a - b);
|
|
202
|
-
const mid = Math.floor(sorted.length / 2);
|
|
203
|
-
const rawMedian = sorted.length % 2 === 0
|
|
204
|
-
? (sorted[mid - 1] + sorted[mid]) / 2
|
|
205
|
-
: sorted[mid];
|
|
206
|
-
return Number(rawMedian.toFixed(3));
|
|
207
|
-
}
|
|
208
133
|
async function mcpCommand(options) {
|
|
209
134
|
// Auto-discover brainfile if not specified
|
|
210
135
|
let defaultFile = options.file;
|
|
@@ -392,72 +317,107 @@ async function mcpCommand(options) {
|
|
|
392
317
|
content: [{ type: 'text', text: JSON.stringify(output, null, 2) }]
|
|
393
318
|
};
|
|
394
319
|
});
|
|
395
|
-
// Search tasks
|
|
396
|
-
server.registerTool('
|
|
397
|
-
title: 'Search
|
|
398
|
-
description: 'Search tasks by
|
|
320
|
+
// Search tool (tasks + logs)
|
|
321
|
+
server.registerTool('search', {
|
|
322
|
+
title: 'Search',
|
|
323
|
+
description: 'Search tasks and logs by query, list recent logs, or view one task/log entry',
|
|
399
324
|
inputSchema: {
|
|
400
325
|
file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
|
|
401
|
-
query: zod_1.z.string().describe('Search query (matches title, description, tags)'),
|
|
326
|
+
query: zod_1.z.string().optional().describe('Search query (matches title, description, tags, and log text in v2)'),
|
|
402
327
|
column: zod_1.z.string().optional().describe('Filter by column ID or name'),
|
|
403
328
|
priority: zod_1.z.enum(['low', 'medium', 'high', 'critical']).optional().describe('Filter by priority'),
|
|
404
|
-
assignee: zod_1.z.string().optional().describe('Filter by assignee')
|
|
329
|
+
assignee: zod_1.z.string().optional().describe('Filter by assignee'),
|
|
330
|
+
recent: zod_1.z.boolean().optional().describe('List recently completed tasks (v2 only)'),
|
|
331
|
+
task: zod_1.z.string().optional().describe('View a specific task/log entry (v2 only)'),
|
|
405
332
|
}
|
|
406
|
-
}, async ({ file, query, column, priority, assignee }) => {
|
|
333
|
+
}, async ({ file, query, column, priority, assignee, recent, task }) => {
|
|
407
334
|
const filePath = file || defaultFile;
|
|
335
|
+
if (task) {
|
|
336
|
+
if (!(0, v2_detect_1.isV2)(filePath)) {
|
|
337
|
+
return { content: [{ type: 'text', text: 'Error: task lookup in search requires v2 per-task file architecture.' }], isError: true };
|
|
338
|
+
}
|
|
339
|
+
const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
|
|
340
|
+
const found = (0, v2_detect_1.findV2Task)(dirs, task, true);
|
|
341
|
+
if (!found) {
|
|
342
|
+
return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
|
|
343
|
+
}
|
|
344
|
+
const output = {
|
|
345
|
+
id: found.doc.task.id,
|
|
346
|
+
title: found.doc.task.title,
|
|
347
|
+
completedAt: found.doc.task.completedAt,
|
|
348
|
+
isLog: found.isLog,
|
|
349
|
+
description: (0, v2_detect_1.extractDescription)(found.doc.body),
|
|
350
|
+
log: (0, v2_detect_1.extractLog)(found.doc.body),
|
|
351
|
+
};
|
|
352
|
+
return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
|
|
353
|
+
}
|
|
354
|
+
if (recent) {
|
|
355
|
+
if (!(0, v2_detect_1.isV2)(filePath)) {
|
|
356
|
+
return { content: [{ type: 'text', text: 'Error: recent log listing requires v2 per-task file architecture.' }], isError: true };
|
|
357
|
+
}
|
|
358
|
+
const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
|
|
359
|
+
const logDocs = (0, core_3.readTasksDir)(dirs.logsDir);
|
|
360
|
+
logDocs.sort((a, b) => (b.task.completedAt || '').localeCompare(a.task.completedAt || ''));
|
|
361
|
+
const logs = logDocs.slice(0, 20).map(doc => ({
|
|
362
|
+
id: doc.task.id,
|
|
363
|
+
title: doc.task.title,
|
|
364
|
+
completedAt: doc.task.completedAt,
|
|
365
|
+
}));
|
|
366
|
+
return { content: [{ type: 'text', text: JSON.stringify({ logs, count: logs.length }, null, 2) }] };
|
|
367
|
+
}
|
|
368
|
+
if (!query) {
|
|
369
|
+
return { content: [{ type: 'text', text: 'Error: query is required unless recent or task is provided' }], isError: true };
|
|
370
|
+
}
|
|
371
|
+
const queryLower = query.toLowerCase();
|
|
408
372
|
// V2: search per-task files
|
|
409
373
|
if ((0, v2_detect_1.isV2)(filePath)) {
|
|
410
374
|
const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
|
|
411
|
-
const
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
for (const doc of taskDocs) {
|
|
415
|
-
const task = doc.task;
|
|
416
|
-
if (column && task.column !== column)
|
|
417
|
-
continue;
|
|
418
|
-
if (priority && task.priority !== priority)
|
|
419
|
-
continue;
|
|
420
|
-
if (assignee && task.assignee !== assignee)
|
|
421
|
-
continue;
|
|
375
|
+
const matches = [];
|
|
376
|
+
const scoreDoc = (doc, includeLogText) => {
|
|
377
|
+
const t = doc.task;
|
|
422
378
|
let score = 0;
|
|
423
|
-
if (
|
|
379
|
+
if (t.title.toLowerCase().includes(queryLower)) {
|
|
424
380
|
score += 10;
|
|
425
|
-
if (
|
|
381
|
+
if (t.title.toLowerCase().startsWith(queryLower))
|
|
426
382
|
score += 5;
|
|
427
383
|
}
|
|
428
|
-
if (
|
|
384
|
+
if (t.description?.toLowerCase().includes(queryLower))
|
|
385
|
+
score += 5;
|
|
386
|
+
if ((0, v2_detect_1.extractDescription)(doc.body)?.toLowerCase().includes(queryLower))
|
|
429
387
|
score += 5;
|
|
430
|
-
if (
|
|
388
|
+
if (t.tags?.some(tag => tag.toLowerCase().includes(queryLower)))
|
|
431
389
|
score += 3;
|
|
432
|
-
if (
|
|
390
|
+
if (includeLogText && (0, v2_detect_1.extractLog)(doc.body)?.toLowerCase().includes(queryLower))
|
|
391
|
+
score += 2;
|
|
392
|
+
if (t.id.toLowerCase() === queryLower)
|
|
433
393
|
score += 20;
|
|
394
|
+
return score;
|
|
395
|
+
};
|
|
396
|
+
const taskDocs = (0, core_3.readTasksDir)(dirs.boardDir);
|
|
397
|
+
for (const doc of taskDocs) {
|
|
398
|
+
const t = doc.task;
|
|
399
|
+
if (column && t.column !== column)
|
|
400
|
+
continue;
|
|
401
|
+
if (priority && t.priority !== priority)
|
|
402
|
+
continue;
|
|
403
|
+
if (assignee && t.assignee !== assignee)
|
|
404
|
+
continue;
|
|
405
|
+
const score = scoreDoc(doc, false);
|
|
434
406
|
if (score > 0) {
|
|
435
|
-
matches.push({ id:
|
|
407
|
+
matches.push({ id: t.id, title: t.title, column: t.column, priority: t.priority, tags: t.tags, assignee: t.assignee, score });
|
|
436
408
|
}
|
|
437
409
|
}
|
|
438
|
-
// Also search logs
|
|
439
410
|
if (!column) {
|
|
440
411
|
const logDocs = (0, core_3.readTasksDir)(dirs.logsDir);
|
|
441
412
|
for (const doc of logDocs) {
|
|
442
|
-
const
|
|
443
|
-
if (priority &&
|
|
413
|
+
const t = doc.task;
|
|
414
|
+
if (priority && t.priority !== priority)
|
|
444
415
|
continue;
|
|
445
|
-
if (assignee &&
|
|
416
|
+
if (assignee && t.assignee !== assignee)
|
|
446
417
|
continue;
|
|
447
|
-
|
|
448
|
-
if (task.title.toLowerCase().includes(queryLower)) {
|
|
449
|
-
score += 10;
|
|
450
|
-
if (task.title.toLowerCase().startsWith(queryLower))
|
|
451
|
-
score += 5;
|
|
452
|
-
}
|
|
453
|
-
if (task.description?.toLowerCase().includes(queryLower))
|
|
454
|
-
score += 5;
|
|
455
|
-
if (task.tags?.some(t => t.toLowerCase().includes(queryLower)))
|
|
456
|
-
score += 3;
|
|
457
|
-
if (task.id.toLowerCase() === queryLower)
|
|
458
|
-
score += 20;
|
|
418
|
+
const score = scoreDoc(doc, true);
|
|
459
419
|
if (score > 0) {
|
|
460
|
-
matches.push({ id:
|
|
420
|
+
matches.push({ id: t.id, title: t.title, column: 'Completed', priority: t.priority, tags: t.tags, assignee: t.assignee, score, isLog: true });
|
|
461
421
|
}
|
|
462
422
|
}
|
|
463
423
|
}
|
|
@@ -470,8 +430,7 @@ async function mcpCommand(options) {
|
|
|
470
430
|
return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
|
|
471
431
|
}
|
|
472
432
|
const { board } = result;
|
|
473
|
-
const
|
|
474
|
-
let matches = [];
|
|
433
|
+
const matches = [];
|
|
475
434
|
for (const col of board.columns) {
|
|
476
435
|
if (column) {
|
|
477
436
|
const matchesId = col.id === column;
|
|
@@ -479,34 +438,33 @@ async function mcpCommand(options) {
|
|
|
479
438
|
if (!matchesId && !matchesName)
|
|
480
439
|
continue;
|
|
481
440
|
}
|
|
482
|
-
for (const
|
|
483
|
-
if (priority &&
|
|
441
|
+
for (const t of col.tasks) {
|
|
442
|
+
if (priority && t.priority !== priority)
|
|
484
443
|
continue;
|
|
485
|
-
if (assignee &&
|
|
444
|
+
if (assignee && t.assignee !== assignee)
|
|
486
445
|
continue;
|
|
487
446
|
let score = 0;
|
|
488
|
-
if (
|
|
447
|
+
if (t.title.toLowerCase().includes(queryLower)) {
|
|
489
448
|
score += 10;
|
|
490
|
-
if (
|
|
449
|
+
if (t.title.toLowerCase().startsWith(queryLower))
|
|
491
450
|
score += 5;
|
|
492
451
|
}
|
|
493
|
-
if (
|
|
452
|
+
if (t.description?.toLowerCase().includes(queryLower))
|
|
494
453
|
score += 5;
|
|
495
|
-
if (
|
|
454
|
+
if (t.tags?.some(tag => tag.toLowerCase().includes(queryLower)))
|
|
496
455
|
score += 3;
|
|
497
|
-
if (
|
|
456
|
+
if (t.id.toLowerCase() === queryLower)
|
|
498
457
|
score += 20;
|
|
499
458
|
if (score > 0) {
|
|
500
|
-
matches.push({ id:
|
|
459
|
+
matches.push({ id: t.id, title: t.title, column: col.title, priority: t.priority, tags: t.tags, assignee: t.assignee, score });
|
|
501
460
|
}
|
|
502
461
|
}
|
|
503
462
|
}
|
|
504
463
|
matches.sort((a, b) => b.score - a.score);
|
|
505
|
-
|
|
506
|
-
return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
|
|
464
|
+
return { content: [{ type: 'text', text: JSON.stringify({ results: matches, count: matches.length }, null, 2) }] };
|
|
507
465
|
});
|
|
508
466
|
// Add task tool
|
|
509
|
-
server.registerTool('
|
|
467
|
+
server.registerTool('task_add', {
|
|
510
468
|
title: 'Add Task',
|
|
511
469
|
description: 'Add a new task to a column in the brainfile',
|
|
512
470
|
inputSchema: {
|
|
@@ -522,7 +480,8 @@ async function mcpCommand(options) {
|
|
|
522
480
|
relatedFiles: zod_1.z.array(zod_1.z.string()).optional().describe('Related file paths'),
|
|
523
481
|
type: zod_1.z.string().optional().describe('Document type (e.g., epic, adr). Determines ID prefix. Default: task'),
|
|
524
482
|
// Contract creation (optional)
|
|
525
|
-
with_contract: zod_1.z.boolean().optional().describe('Attach a contract to the new task (status=ready)'),
|
|
483
|
+
with_contract: zod_1.z.boolean().optional().describe('Attach a contract to the new task (default status=draft; use ready:true to make immediately dispatchable)'),
|
|
484
|
+
ready: zod_1.z.boolean().optional().describe('When true, contract status is set to ready instead of draft'),
|
|
526
485
|
deliverables: zod_1.z.array(zod_1.z.string()).optional().describe('Contract deliverables: type:path:description'),
|
|
527
486
|
validation_commands: zod_1.z.array(zod_1.z.string()).optional().describe('Contract validation commands'),
|
|
528
487
|
constraints: zod_1.z.array(zod_1.z.string()).optional().describe('Contract constraints'),
|
|
@@ -530,7 +489,7 @@ async function mcpCommand(options) {
|
|
|
530
489
|
withContract: zod_1.z.boolean().optional().describe('Alias of with_contract'),
|
|
531
490
|
validationCommands: zod_1.z.array(zod_1.z.string()).optional().describe('Alias of validation_commands'),
|
|
532
491
|
}
|
|
533
|
-
}, async ({ file, column, title, description, priority, tags, assignee, dueDate, subtasks, relatedFiles, type: docType, with_contract, deliverables, validation_commands, constraints, withContract, validationCommands, }) => {
|
|
492
|
+
}, async ({ file, column, title, description, priority, tags, assignee, dueDate, subtasks, relatedFiles, type: docType, with_contract, ready: contractReady, deliverables, validation_commands, constraints, withContract, validationCommands, }) => {
|
|
534
493
|
const filePath = file || defaultFile;
|
|
535
494
|
// V2: add task as individual file
|
|
536
495
|
if ((0, v2_detect_1.isV2)(filePath)) {
|
|
@@ -570,7 +529,7 @@ async function mcpCommand(options) {
|
|
|
570
529
|
...(builtSubtasks && { subtasks: builtSubtasks }),
|
|
571
530
|
createdAt: new Date().toISOString(),
|
|
572
531
|
};
|
|
573
|
-
// Optionally attach contract
|
|
532
|
+
// Optionally attach contract (default status=draft; ready:true → status=ready)
|
|
574
533
|
const wantsContract = Boolean(with_contract ?? withContract) ||
|
|
575
534
|
Boolean(deliverables && deliverables.length > 0) ||
|
|
576
535
|
Boolean(validation_commands && validation_commands.length > 0) ||
|
|
@@ -581,6 +540,7 @@ async function mcpCommand(options) {
|
|
|
581
540
|
deliverableSpecs: deliverables,
|
|
582
541
|
validationCommands: validation_commands ?? validationCommands,
|
|
583
542
|
constraints,
|
|
543
|
+
status: contractReady ? 'ready' : 'draft',
|
|
584
544
|
});
|
|
585
545
|
task.contract = contract;
|
|
586
546
|
}
|
|
@@ -636,6 +596,7 @@ async function mcpCommand(options) {
|
|
|
636
596
|
deliverableSpecs: deliverables,
|
|
637
597
|
validationCommands: validation_commands ?? validationCommands,
|
|
638
598
|
constraints,
|
|
599
|
+
status: contractReady ? 'ready' : 'draft',
|
|
639
600
|
});
|
|
640
601
|
const contractResult = (0, core_1.setTaskContract)(nextBoard, newTask.id, contract);
|
|
641
602
|
if (!contractResult.success || !contractResult.board) {
|
|
@@ -652,93 +613,27 @@ async function mcpCommand(options) {
|
|
|
652
613
|
content: [{ type: 'text', text: `Task added successfully: ${newTask.id} - ${newTask.title}` }]
|
|
653
614
|
};
|
|
654
615
|
});
|
|
655
|
-
// Attach contract tool
|
|
656
|
-
server.registerTool('attach_contract', {
|
|
657
|
-
title: 'Attach Contract',
|
|
658
|
-
description: 'Attach a new contract to an existing task (status=ready)',
|
|
659
|
-
inputSchema: {
|
|
660
|
-
file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
|
|
661
|
-
task: zod_1.z.string().optional().describe('Task ID to attach contract to'),
|
|
662
|
-
task_id: zod_1.z.string().optional().describe('Alias of task'),
|
|
663
|
-
deliverables: zod_1.z.array(zod_1.z.string()).optional().describe('Contract deliverables: type:path:description'),
|
|
664
|
-
validation_commands: zod_1.z.array(zod_1.z.string()).optional().describe('Contract validation commands'),
|
|
665
|
-
constraints: zod_1.z.array(zod_1.z.string()).optional().describe('Contract constraints'),
|
|
666
|
-
validationCommands: zod_1.z.array(zod_1.z.string()).optional().describe('Alias of validation_commands'),
|
|
667
|
-
}
|
|
668
|
-
}, async ({ file, task, task_id, deliverables, validation_commands, constraints, validationCommands }) => {
|
|
669
|
-
const filePath = file || defaultFile;
|
|
670
|
-
const resolvedTaskId = task || task_id;
|
|
671
|
-
if (!resolvedTaskId) {
|
|
672
|
-
return { content: [{ type: 'text', text: 'Error: task is required' }], isError: true };
|
|
673
|
-
}
|
|
674
|
-
// V2: update task file directly
|
|
675
|
-
if ((0, v2_detect_1.isV2)(filePath)) {
|
|
676
|
-
const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
|
|
677
|
-
const found = (0, v2_detect_1.findV2Task)(dirs, resolvedTaskId);
|
|
678
|
-
if (!found) {
|
|
679
|
-
return { content: [{ type: 'text', text: `Error: Task not found: ${resolvedTaskId}` }], isError: true };
|
|
680
|
-
}
|
|
681
|
-
try {
|
|
682
|
-
const contract = (0, contractSpec_1.buildContract)({
|
|
683
|
-
deliverableSpecs: deliverables,
|
|
684
|
-
validationCommands: validation_commands ?? validationCommands,
|
|
685
|
-
constraints,
|
|
686
|
-
});
|
|
687
|
-
found.doc.task.contract = contract;
|
|
688
|
-
found.doc.task.updatedAt = new Date().toISOString();
|
|
689
|
-
(0, core_3.writeTaskFile)(found.filePath, found.doc.task, found.doc.body);
|
|
690
|
-
return { content: [{ type: 'text', text: `Contract attached: ${resolvedTaskId}` }] };
|
|
691
|
-
}
|
|
692
|
-
catch (e) {
|
|
693
|
-
return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
// V1: use board
|
|
697
|
-
const result = readBoard(filePath);
|
|
698
|
-
if ('error' in result) {
|
|
699
|
-
return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
|
|
700
|
-
}
|
|
701
|
-
let { board } = result;
|
|
702
|
-
const taskInfo = (0, core_1.findTaskById)(board, resolvedTaskId);
|
|
703
|
-
if (!taskInfo) {
|
|
704
|
-
return { content: [{ type: 'text', text: `Error: Task not found: ${resolvedTaskId}` }], isError: true };
|
|
705
|
-
}
|
|
706
|
-
try {
|
|
707
|
-
const contract = (0, contractSpec_1.buildContract)({
|
|
708
|
-
deliverableSpecs: deliverables,
|
|
709
|
-
validationCommands: validation_commands ?? validationCommands,
|
|
710
|
-
constraints,
|
|
711
|
-
});
|
|
712
|
-
const contractResult = (0, core_1.setTaskContract)(board, resolvedTaskId, contract);
|
|
713
|
-
if (!contractResult.success || !contractResult.board) {
|
|
714
|
-
return { content: [{ type: 'text', text: `Error: ${contractResult.error || 'Failed to attach contract'}` }], isError: true };
|
|
715
|
-
}
|
|
716
|
-
writeBoard(filePath, contractResult.board);
|
|
717
|
-
return { content: [{ type: 'text', text: `Contract attached: ${resolvedTaskId}` }] };
|
|
718
|
-
}
|
|
719
|
-
catch (e) {
|
|
720
|
-
return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
|
|
721
|
-
}
|
|
722
|
-
});
|
|
723
616
|
// Move task tool
|
|
724
|
-
server.registerTool('
|
|
617
|
+
server.registerTool('task_move', {
|
|
725
618
|
title: 'Move Task',
|
|
726
619
|
description: 'Move a task to a different column',
|
|
727
620
|
inputSchema: {
|
|
728
621
|
file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
|
|
729
|
-
|
|
622
|
+
taskId: zod_1.z.union([zod_1.z.string(), zod_1.z.array(zod_1.z.string())]).optional().describe('Task ID or array of task IDs to move'),
|
|
623
|
+
task: zod_1.z.string().optional().describe('Alias of taskId for single task move'),
|
|
730
624
|
column: zod_1.z.string().describe('Target column ID or name')
|
|
731
625
|
}
|
|
732
|
-
}, async ({ file, task, column }) => {
|
|
626
|
+
}, async ({ file, taskId, task, column }) => {
|
|
733
627
|
const filePath = file || defaultFile;
|
|
628
|
+
const rawTaskIds = taskId ?? task;
|
|
629
|
+
if (!rawTaskIds) {
|
|
630
|
+
return { content: [{ type: 'text', text: 'Error: taskId is required' }], isError: true };
|
|
631
|
+
}
|
|
632
|
+
const taskIds = Array.isArray(rawTaskIds) ? rawTaskIds : [rawTaskIds];
|
|
633
|
+
const isBatch = taskIds.length > 1;
|
|
734
634
|
// V2: update task file
|
|
735
635
|
if ((0, v2_detect_1.isV2)(filePath)) {
|
|
736
636
|
const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
|
|
737
|
-
const taskPath = path.join(dirs.boardDir, (0, core_3.taskFileName)(task));
|
|
738
|
-
const doc = (0, core_3.readTaskFile)(taskPath);
|
|
739
|
-
if (!doc) {
|
|
740
|
-
return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
|
|
741
|
-
}
|
|
742
637
|
const board = (0, v2_detect_1.readV2BoardConfig)(filePath);
|
|
743
638
|
let targetColumn = board.columns.find(c => c.id === column);
|
|
744
639
|
if (!targetColumn)
|
|
@@ -749,38 +644,60 @@ async function mcpCommand(options) {
|
|
|
749
644
|
return mcpStructuredError(columnValidation.error || `Invalid column: ${targetColumnId}`, 'column', targetColumnId);
|
|
750
645
|
}
|
|
751
646
|
const resolvedTargetColumn = targetColumn || { id: column, title: column, tasks: [] };
|
|
752
|
-
|
|
753
|
-
const
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
647
|
+
let nextPosition = (0, core_3.readTasksDir)(dirs.boardDir).filter(t => t.task.column === resolvedTargetColumn.id).length;
|
|
648
|
+
const results = [];
|
|
649
|
+
for (const id of taskIds) {
|
|
650
|
+
const taskPath = path.join(dirs.boardDir, (0, core_3.taskFileName)(id));
|
|
651
|
+
const doc = (0, core_3.readTaskFile)(taskPath);
|
|
652
|
+
if (!doc) {
|
|
653
|
+
results.push({ taskId: id, success: false, error: `Task not found: ${id}` });
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
const sourceColumn = doc.task.column || '';
|
|
657
|
+
doc.task.column = resolvedTargetColumn.id;
|
|
658
|
+
doc.task.position = nextPosition++;
|
|
659
|
+
doc.task.updatedAt = new Date().toISOString();
|
|
660
|
+
(0, core_3.writeTaskFile)(taskPath, doc.task, doc.body);
|
|
661
|
+
const shouldAutoComplete = resolvedTargetColumn.completionColumn === true &&
|
|
662
|
+
isTaskCompletable(doc.task.type, board.types);
|
|
663
|
+
if (shouldAutoComplete) {
|
|
664
|
+
const completeResult = (0, core_3.completeTaskFile)(taskPath, dirs.logsDir);
|
|
665
|
+
if (!completeResult.success) {
|
|
666
|
+
results.push({ taskId: id, success: false, error: completeResult.error || `Failed to complete task: ${id}` });
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
const warning = (0, errorHandler_1.mcpCheckIncompleteSubtasks)(doc.task, resolvedTargetColumn);
|
|
671
|
+
let message = `Task ${id} moved from "${sourceColumn}" to "${resolvedTargetColumn.title}"`;
|
|
672
|
+
if (shouldAutoComplete) {
|
|
673
|
+
message += '\nTask auto-completed and moved to logs/.';
|
|
674
|
+
}
|
|
675
|
+
results.push({ taskId: id, success: true, message, warning: warning?.warning });
|
|
769
676
|
}
|
|
770
|
-
if (
|
|
771
|
-
|
|
772
|
-
|
|
677
|
+
if (!isBatch) {
|
|
678
|
+
const single = results[0];
|
|
679
|
+
if (!single?.success) {
|
|
680
|
+
return { content: [{ type: 'text', text: `Error: ${single?.error || 'Move failed'}` }], isError: true };
|
|
681
|
+
}
|
|
682
|
+
let text = single.message || `Task ${taskIds[0]} moved`;
|
|
683
|
+
if (single.warning)
|
|
684
|
+
text += `\n\n${single.warning}`;
|
|
685
|
+
return { content: [{ type: 'text', text }] };
|
|
686
|
+
}
|
|
687
|
+
const successCount = results.filter(r => r.success).length;
|
|
688
|
+
const failureCount = results.length - successCount;
|
|
689
|
+
const output = { success: failureCount === 0, successCount, failureCount, results };
|
|
690
|
+
return {
|
|
691
|
+
content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
|
|
692
|
+
isError: failureCount > 0 && successCount === 0,
|
|
693
|
+
};
|
|
773
694
|
}
|
|
774
695
|
// V1: use board
|
|
775
696
|
const result = readBoard(filePath);
|
|
776
697
|
if ('error' in result) {
|
|
777
698
|
return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
|
|
778
699
|
}
|
|
779
|
-
|
|
780
|
-
const taskInfo = (0, core_1.findTaskById)(board, task);
|
|
781
|
-
if (!taskInfo) {
|
|
782
|
-
return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
|
|
783
|
-
}
|
|
700
|
+
const { board } = result;
|
|
784
701
|
let targetColumn = (0, core_1.findColumnById)(board, column);
|
|
785
702
|
if (!targetColumn) {
|
|
786
703
|
targetColumn = (0, core_1.findColumnByName)(board, column);
|
|
@@ -788,27 +705,64 @@ async function mcpCommand(options) {
|
|
|
788
705
|
if (!targetColumn) {
|
|
789
706
|
return { content: [{ type: 'text', text: `Error: Column not found: ${column}` }], isError: true };
|
|
790
707
|
}
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
708
|
+
if (!isBatch) {
|
|
709
|
+
const id = taskIds[0];
|
|
710
|
+
const taskInfo = (0, core_1.findTaskById)(board, id);
|
|
711
|
+
if (!taskInfo) {
|
|
712
|
+
return { content: [{ type: 'text', text: `Error: Task not found: ${id}` }], isError: true };
|
|
713
|
+
}
|
|
714
|
+
const moveResult = (0, core_1.moveTask)(board, id, taskInfo.column.id, targetColumn.id, targetColumn.tasks.length);
|
|
715
|
+
if (!moveResult.success) {
|
|
716
|
+
return { content: [{ type: 'text', text: `Error: ${moveResult.error}` }], isError: true };
|
|
717
|
+
}
|
|
718
|
+
writeBoard(filePath, moveResult.board);
|
|
719
|
+
const warning = (0, errorHandler_1.mcpCheckIncompleteSubtasks)(taskInfo.task, targetColumn);
|
|
720
|
+
let message = `Task ${id} moved from "${taskInfo.column.title}" to "${targetColumn.title}"`;
|
|
721
|
+
if (warning)
|
|
722
|
+
message += `\n\n${warning.warning}`;
|
|
723
|
+
return { content: [{ type: 'text', text: message }] };
|
|
724
|
+
}
|
|
725
|
+
const tasksWithIncomplete = [];
|
|
726
|
+
for (const id of taskIds) {
|
|
727
|
+
const taskInfo = (0, core_1.findTaskById)(board, id);
|
|
728
|
+
if (taskInfo) {
|
|
729
|
+
const warning = (0, errorHandler_1.mcpCheckIncompleteSubtasks)(taskInfo.task, targetColumn);
|
|
730
|
+
if (warning?.incompleteSubtasks) {
|
|
731
|
+
tasksWithIncomplete.push({
|
|
732
|
+
id,
|
|
733
|
+
incomplete: warning.incompleteSubtasks.incomplete.length,
|
|
734
|
+
total: warning.incompleteSubtasks.total,
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
const bulkResult = (0, core_1.moveTasks)(board, taskIds, targetColumn.id);
|
|
740
|
+
if (bulkResult.board) {
|
|
741
|
+
writeBoard(filePath, bulkResult.board);
|
|
794
742
|
}
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
743
|
+
const output = {
|
|
744
|
+
success: bulkResult.success,
|
|
745
|
+
successCount: bulkResult.successCount,
|
|
746
|
+
failureCount: bulkResult.failureCount,
|
|
747
|
+
results: bulkResult.results,
|
|
748
|
+
};
|
|
749
|
+
if (tasksWithIncomplete.length > 0) {
|
|
750
|
+
output.warning = `${tasksWithIncomplete.length} task(s) moved to "${targetColumn.title}" have incomplete subtasks`;
|
|
751
|
+
output.tasksWithIncompleteSubtasks = tasksWithIncomplete;
|
|
800
752
|
}
|
|
801
753
|
return {
|
|
802
|
-
content: [{ type: 'text', text:
|
|
754
|
+
content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
|
|
755
|
+
isError: !bulkResult.success,
|
|
803
756
|
};
|
|
804
757
|
});
|
|
805
758
|
// Patch task tool
|
|
806
|
-
server.registerTool('
|
|
759
|
+
server.registerTool('task_patch', {
|
|
807
760
|
title: 'Patch Task',
|
|
808
761
|
description: 'Update specific fields of a task. Set fields to null to remove them.',
|
|
809
762
|
inputSchema: {
|
|
810
763
|
file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
|
|
811
|
-
|
|
764
|
+
taskId: zod_1.z.union([zod_1.z.string(), zod_1.z.array(zod_1.z.string())]).optional().describe('Task ID or array of task IDs to update'),
|
|
765
|
+
task: zod_1.z.string().optional().describe('Alias of taskId for single task update'),
|
|
812
766
|
title: zod_1.z.string().optional().describe('New task title'),
|
|
813
767
|
description: zod_1.z.string().nullable().optional().describe('New description (null to remove)'),
|
|
814
768
|
priority: zod_1.z.enum(['low', 'medium', 'high', 'critical']).nullable().optional().describe('New priority (null to remove)'),
|
|
@@ -817,59 +771,83 @@ async function mcpCommand(options) {
|
|
|
817
771
|
dueDate: zod_1.z.string().nullable().optional().describe('New due date (null to remove)'),
|
|
818
772
|
relatedFiles: zod_1.z.array(zod_1.z.string()).nullable().optional().describe('Related file paths (null to remove)')
|
|
819
773
|
}
|
|
820
|
-
}, async ({ file, task, title, description, priority, tags, assignee, dueDate, relatedFiles }) => {
|
|
774
|
+
}, async ({ file, taskId, task, title, description, priority, tags, assignee, dueDate, relatedFiles }) => {
|
|
821
775
|
const filePath = file || defaultFile;
|
|
776
|
+
const rawTaskIds = taskId ?? task;
|
|
777
|
+
if (!rawTaskIds) {
|
|
778
|
+
return { content: [{ type: 'text', text: 'Error: taskId is required' }], isError: true };
|
|
779
|
+
}
|
|
780
|
+
const taskIds = Array.isArray(rawTaskIds) ? rawTaskIds : [rawTaskIds];
|
|
781
|
+
const isBatch = taskIds.length > 1;
|
|
822
782
|
const isNull = (v) => v === null || v === 'null';
|
|
823
783
|
// V2: update task file directly
|
|
824
784
|
if ((0, v2_detect_1.isV2)(filePath)) {
|
|
825
785
|
const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
|
|
826
|
-
const
|
|
827
|
-
const
|
|
828
|
-
|
|
829
|
-
|
|
786
|
+
const results = [];
|
|
787
|
+
for (const id of taskIds) {
|
|
788
|
+
const taskPath = path.join(dirs.boardDir, (0, core_3.taskFileName)(id));
|
|
789
|
+
const doc = (0, core_3.readTaskFile)(taskPath);
|
|
790
|
+
if (!doc) {
|
|
791
|
+
results.push({ taskId: id, success: false, error: 'Task not found' });
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
const t = doc.task;
|
|
795
|
+
if (title !== undefined)
|
|
796
|
+
t.title = title;
|
|
797
|
+
if (description !== undefined) {
|
|
798
|
+
if (isNull(description))
|
|
799
|
+
delete t.description;
|
|
800
|
+
else
|
|
801
|
+
t.description = description;
|
|
802
|
+
}
|
|
803
|
+
if (priority !== undefined) {
|
|
804
|
+
if (isNull(priority))
|
|
805
|
+
delete t.priority;
|
|
806
|
+
else
|
|
807
|
+
t.priority = priority;
|
|
808
|
+
}
|
|
809
|
+
if (tags !== undefined) {
|
|
810
|
+
if (isNull(tags))
|
|
811
|
+
delete t.tags;
|
|
812
|
+
else
|
|
813
|
+
t.tags = tags;
|
|
814
|
+
}
|
|
815
|
+
if (assignee !== undefined) {
|
|
816
|
+
if (isNull(assignee))
|
|
817
|
+
delete t.assignee;
|
|
818
|
+
else
|
|
819
|
+
t.assignee = assignee;
|
|
820
|
+
}
|
|
821
|
+
if (dueDate !== undefined) {
|
|
822
|
+
if (isNull(dueDate))
|
|
823
|
+
delete t.dueDate;
|
|
824
|
+
else
|
|
825
|
+
t.dueDate = dueDate;
|
|
826
|
+
}
|
|
827
|
+
if (relatedFiles !== undefined) {
|
|
828
|
+
if (isNull(relatedFiles))
|
|
829
|
+
delete t.relatedFiles;
|
|
830
|
+
else
|
|
831
|
+
t.relatedFiles = relatedFiles;
|
|
832
|
+
}
|
|
833
|
+
t.updatedAt = new Date().toISOString();
|
|
834
|
+
(0, core_3.writeTaskFile)(taskPath, t, doc.body);
|
|
835
|
+
results.push({ taskId: id, success: true });
|
|
830
836
|
}
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
}
|
|
846
|
-
if (tags !== undefined) {
|
|
847
|
-
if (isNull(tags))
|
|
848
|
-
delete t.tags;
|
|
849
|
-
else
|
|
850
|
-
t.tags = tags;
|
|
851
|
-
}
|
|
852
|
-
if (assignee !== undefined) {
|
|
853
|
-
if (isNull(assignee))
|
|
854
|
-
delete t.assignee;
|
|
855
|
-
else
|
|
856
|
-
t.assignee = assignee;
|
|
857
|
-
}
|
|
858
|
-
if (dueDate !== undefined) {
|
|
859
|
-
if (isNull(dueDate))
|
|
860
|
-
delete t.dueDate;
|
|
861
|
-
else
|
|
862
|
-
t.dueDate = dueDate;
|
|
863
|
-
}
|
|
864
|
-
if (relatedFiles !== undefined) {
|
|
865
|
-
if (isNull(relatedFiles))
|
|
866
|
-
delete t.relatedFiles;
|
|
867
|
-
else
|
|
868
|
-
t.relatedFiles = relatedFiles;
|
|
869
|
-
}
|
|
870
|
-
t.updatedAt = new Date().toISOString();
|
|
871
|
-
(0, core_3.writeTaskFile)(taskPath, t, doc.body);
|
|
872
|
-
return { content: [{ type: 'text', text: `Task ${task} updated successfully` }] };
|
|
837
|
+
if (!isBatch) {
|
|
838
|
+
const single = results[0];
|
|
839
|
+
if (!single?.success) {
|
|
840
|
+
return { content: [{ type: 'text', text: `Error: ${single?.error || 'Task not found'}` }], isError: true };
|
|
841
|
+
}
|
|
842
|
+
return { content: [{ type: 'text', text: `Task ${taskIds[0]} updated successfully` }] };
|
|
843
|
+
}
|
|
844
|
+
const successCount = results.filter(r => r.success).length;
|
|
845
|
+
const failureCount = results.length - successCount;
|
|
846
|
+
const output = { success: failureCount === 0, successCount, failureCount, results };
|
|
847
|
+
return {
|
|
848
|
+
content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
|
|
849
|
+
isError: failureCount > 0 && successCount === 0,
|
|
850
|
+
};
|
|
873
851
|
}
|
|
874
852
|
// V1: use board
|
|
875
853
|
const result = readBoard(filePath);
|
|
@@ -892,17 +870,32 @@ async function mcpCommand(options) {
|
|
|
892
870
|
patch.dueDate = isNull(dueDate) ? undefined : dueDate;
|
|
893
871
|
if (relatedFiles !== undefined)
|
|
894
872
|
patch.relatedFiles = isNull(relatedFiles) ? undefined : relatedFiles;
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
873
|
+
if (!isBatch) {
|
|
874
|
+
const id = taskIds[0];
|
|
875
|
+
const patchResult = (0, core_1.patchTask)(board, id, patch);
|
|
876
|
+
if (!patchResult.success) {
|
|
877
|
+
return { content: [{ type: 'text', text: `Error: ${patchResult.error}` }], isError: true };
|
|
878
|
+
}
|
|
879
|
+
writeBoard(filePath, patchResult.board);
|
|
880
|
+
return { content: [{ type: 'text', text: `Task ${id} updated successfully` }] };
|
|
881
|
+
}
|
|
882
|
+
const bulkResult = (0, core_1.patchTasks)(board, taskIds, patch);
|
|
883
|
+
if (bulkResult.board) {
|
|
884
|
+
writeBoard(filePath, bulkResult.board);
|
|
898
885
|
}
|
|
899
|
-
|
|
886
|
+
const output = {
|
|
887
|
+
success: bulkResult.success,
|
|
888
|
+
successCount: bulkResult.successCount,
|
|
889
|
+
failureCount: bulkResult.failureCount,
|
|
890
|
+
results: bulkResult.results,
|
|
891
|
+
};
|
|
900
892
|
return {
|
|
901
|
-
content: [{ type: 'text', text:
|
|
893
|
+
content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
|
|
894
|
+
isError: !bulkResult.success,
|
|
902
895
|
};
|
|
903
896
|
});
|
|
904
897
|
// Delete task tool
|
|
905
|
-
server.registerTool('
|
|
898
|
+
server.registerTool('task_delete', {
|
|
906
899
|
title: 'Delete Task',
|
|
907
900
|
description: 'Permanently delete a task from the brainfile',
|
|
908
901
|
inputSchema: {
|
|
@@ -946,212 +939,790 @@ async function mcpCommand(options) {
|
|
|
946
939
|
content: [{ type: 'text', text: `Task ${task} deleted successfully` }]
|
|
947
940
|
};
|
|
948
941
|
});
|
|
949
|
-
//
|
|
950
|
-
server.registerTool('
|
|
951
|
-
title: '
|
|
952
|
-
description: '
|
|
942
|
+
// Unified subtask tool (action-based)
|
|
943
|
+
server.registerTool('subtask', {
|
|
944
|
+
title: 'Subtask',
|
|
945
|
+
description: 'Unified subtask tool for add/toggle/delete/update with single, array, or all targeting',
|
|
953
946
|
inputSchema: {
|
|
947
|
+
action: zod_1.z.enum(['add', 'toggle', 'delete', 'update']).describe('Subtask action'),
|
|
954
948
|
file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
|
|
955
|
-
task: zod_1.z.string().describe('
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
949
|
+
task: zod_1.z.string().describe('Parent task ID'),
|
|
950
|
+
subtask: zod_1.z.string().optional().describe('Single subtask title/id depending on action'),
|
|
951
|
+
subtasks: zod_1.z.array(zod_1.z.string()).optional().describe('Subtask titles/ids depending on action'),
|
|
952
|
+
title: zod_1.z.string().optional().describe('New title for update action'),
|
|
953
|
+
titles: zod_1.z.array(zod_1.z.string()).optional().describe('Optional titles for batch update action'),
|
|
954
|
+
completed: zod_1.z.boolean().optional().describe('For toggle action: set explicit completed state (true/false) instead of flipping'),
|
|
955
|
+
all: zod_1.z.boolean().optional().describe('For toggle/delete action: target all subtasks in the task'),
|
|
956
|
+
}
|
|
957
|
+
}, async ({ action, file, task, subtask, subtasks, title, titles, completed, all }) => {
|
|
959
958
|
const filePath = file || defaultFile;
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
959
|
+
const listParam = subtasks ?? (subtask ? [subtask] : []);
|
|
960
|
+
const useAll = all === true;
|
|
961
|
+
const resolveUpdateTitles = (ids) => {
|
|
962
|
+
if (titles && titles.length > 0) {
|
|
963
|
+
if (titles.length !== ids.length && titles.length !== 1) {
|
|
964
|
+
return { ok: false, error: 'titles length must match subtasks length (or provide a single title to apply to all)' };
|
|
965
|
+
}
|
|
966
|
+
const values = titles.length === 1 ? ids.map(() => titles[0]) : titles;
|
|
967
|
+
return { ok: true, values };
|
|
966
968
|
}
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
969
|
+
if (title !== undefined) {
|
|
970
|
+
return { ok: true, values: ids.map(() => title) };
|
|
971
|
+
}
|
|
972
|
+
return { ok: false, error: 'title or titles is required for action=update' };
|
|
973
|
+
};
|
|
974
|
+
// ── add ────────────────────────────────────────────────────────────────
|
|
975
|
+
if (action === 'add') {
|
|
976
|
+
const titlesToAdd = listParam.map(value => value.trim()).filter(Boolean);
|
|
977
|
+
if (titlesToAdd.length === 0) {
|
|
978
|
+
return { content: [{ type: 'text', text: 'Error: subtask or subtasks is required for action=add' }], isError: true };
|
|
979
|
+
}
|
|
980
|
+
if ((0, v2_detect_1.isV2)(filePath)) {
|
|
981
|
+
const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
|
|
982
|
+
const found = (0, v2_detect_1.findV2Task)(dirs, task);
|
|
983
|
+
if (!found) {
|
|
984
|
+
return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
|
|
975
985
|
}
|
|
976
|
-
const
|
|
977
|
-
if (!
|
|
978
|
-
|
|
986
|
+
const t = found.doc.task;
|
|
987
|
+
if (!t.subtasks)
|
|
988
|
+
t.subtasks = [];
|
|
989
|
+
let nextIndex = t.subtasks.length > 0
|
|
990
|
+
? Math.max(...t.subtasks.map(st => parseInt(st.id.split('-').pop() || '0', 10))) + 1
|
|
991
|
+
: 1;
|
|
992
|
+
const added = [];
|
|
993
|
+
for (const value of titlesToAdd) {
|
|
994
|
+
const id = `${task}-${nextIndex++}`;
|
|
995
|
+
const newSubtask = { id, title: value, completed: false };
|
|
996
|
+
t.subtasks.push(newSubtask);
|
|
997
|
+
added.push({ id: newSubtask.id, title: newSubtask.title });
|
|
979
998
|
}
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
});
|
|
985
|
-
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' });
|
|
986
|
-
if (!ghResult.success) {
|
|
987
|
-
return { content: [{ type: 'text', text: `Error creating GitHub issue: ${ghResult.error}` }], isError: true };
|
|
999
|
+
t.updatedAt = new Date().toISOString();
|
|
1000
|
+
(0, core_3.writeTaskFile)(found.filePath, t, found.doc.body);
|
|
1001
|
+
if (added.length === 1) {
|
|
1002
|
+
return { content: [{ type: 'text', text: `Subtask added: ${added[0].id} - ${added[0].title}` }] };
|
|
988
1003
|
}
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
if (
|
|
993
|
-
|
|
994
|
-
|
|
1004
|
+
return { content: [{ type: 'text', text: JSON.stringify({ added, count: added.length }, null, 2) }] };
|
|
1005
|
+
}
|
|
1006
|
+
const result = readBoard(filePath);
|
|
1007
|
+
if ('error' in result) {
|
|
1008
|
+
return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
|
|
1009
|
+
}
|
|
1010
|
+
let board = result.board;
|
|
1011
|
+
const added = [];
|
|
1012
|
+
for (const value of titlesToAdd) {
|
|
1013
|
+
const addResult = (0, core_1.addSubtask)(board, task, value);
|
|
1014
|
+
if (!addResult.success || !addResult.board) {
|
|
1015
|
+
return { content: [{ type: 'text', text: `Error: ${addResult.error}` }], isError: true };
|
|
995
1016
|
}
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1017
|
+
board = addResult.board;
|
|
1018
|
+
const updatedTask = (0, core_1.findTaskById)(board, task)?.task;
|
|
1019
|
+
const created = updatedTask?.subtasks?.slice(-1)[0];
|
|
1020
|
+
if (created)
|
|
1021
|
+
added.push({ id: created.id, title: created.title });
|
|
1022
|
+
}
|
|
1023
|
+
writeBoard(filePath, board);
|
|
1024
|
+
if (added.length === 1) {
|
|
1025
|
+
return { content: [{ type: 'text', text: `Subtask added: ${added[0].id} - ${added[0].title}` }] };
|
|
1026
|
+
}
|
|
1027
|
+
return { content: [{ type: 'text', text: JSON.stringify({ added, count: added.length }, null, 2) }] };
|
|
1028
|
+
}
|
|
1029
|
+
// ── delete ─────────────────────────────────────────────────────────────
|
|
1030
|
+
if (action === 'delete') {
|
|
1031
|
+
if ((0, v2_detect_1.isV2)(filePath)) {
|
|
1032
|
+
const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
|
|
1033
|
+
const found = (0, v2_detect_1.findV2Task)(dirs, task);
|
|
1034
|
+
if (!found) {
|
|
1035
|
+
return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
|
|
1010
1036
|
}
|
|
1011
|
-
const
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
});
|
|
1015
|
-
const linearResult = await (0, linear_auth_1.createLinearIssue)({ teamId, title: payload.title, description: payload.description, priority: payload.priority, labelNames: payload.labelNames, stateName: 'Done' });
|
|
1016
|
-
if (!linearResult.success) {
|
|
1017
|
-
return { content: [{ type: 'text', text: `Error creating Linear issue: ${linearResult.error}` }], isError: true };
|
|
1037
|
+
const t = found.doc.task;
|
|
1038
|
+
if (!t.subtasks || t.subtasks.length === 0) {
|
|
1039
|
+
return { content: [{ type: 'text', text: `Error: Task has no subtasks` }], isError: true };
|
|
1018
1040
|
}
|
|
1019
|
-
|
|
1020
|
-
|
|
1041
|
+
const targetIds = useAll ? t.subtasks.map(st => st.id) : listParam;
|
|
1042
|
+
if (targetIds.length === 0) {
|
|
1043
|
+
return { content: [{ type: 'text', text: 'Error: subtask or subtasks is required for action=delete (unless all=true)' }], isError: true };
|
|
1044
|
+
}
|
|
1045
|
+
const existing = new Set(t.subtasks.map(st => st.id));
|
|
1046
|
+
const deleted = targetIds.filter(id => existing.has(id));
|
|
1047
|
+
const missing = targetIds.filter(id => !existing.has(id));
|
|
1048
|
+
if (deleted.length === 0) {
|
|
1049
|
+
return { content: [{ type: 'text', text: `Error: Subtask not found: ${targetIds.join(', ')}` }], isError: true };
|
|
1050
|
+
}
|
|
1051
|
+
const deleteSet = new Set(deleted);
|
|
1052
|
+
t.subtasks = t.subtasks.filter(st => !deleteSet.has(st.id));
|
|
1053
|
+
t.updatedAt = new Date().toISOString();
|
|
1054
|
+
(0, core_3.writeTaskFile)(found.filePath, t, found.doc.body);
|
|
1055
|
+
if (!useAll && deleted.length === 1 && missing.length === 0) {
|
|
1056
|
+
return { content: [{ type: 'text', text: `Subtask ${deleted[0]} deleted successfully` }] };
|
|
1057
|
+
}
|
|
1058
|
+
return { content: [{ type: 'text', text: JSON.stringify({ deleted, missing, count: deleted.length }, null, 2) }] };
|
|
1021
1059
|
}
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
(0,
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1060
|
+
const result = readBoard(filePath);
|
|
1061
|
+
if ('error' in result) {
|
|
1062
|
+
return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
|
|
1063
|
+
}
|
|
1064
|
+
let board = result.board;
|
|
1065
|
+
const taskInfo = (0, core_1.findTaskById)(board, task);
|
|
1066
|
+
if (!taskInfo) {
|
|
1067
|
+
return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
|
|
1068
|
+
}
|
|
1069
|
+
const targetIds = useAll ? (taskInfo.task.subtasks || []).map(st => st.id) : listParam;
|
|
1070
|
+
if (targetIds.length === 0) {
|
|
1071
|
+
return { content: [{ type: 'text', text: 'Error: subtask or subtasks is required for action=delete (unless all=true)' }], isError: true };
|
|
1072
|
+
}
|
|
1073
|
+
const deleted = [];
|
|
1074
|
+
const missing = [];
|
|
1075
|
+
for (const id of targetIds) {
|
|
1076
|
+
const deleteResult = (0, core_1.deleteSubtask)(board, task, id);
|
|
1077
|
+
if (!deleteResult.success || !deleteResult.board) {
|
|
1078
|
+
missing.push(id);
|
|
1079
|
+
continue;
|
|
1080
|
+
}
|
|
1081
|
+
board = deleteResult.board;
|
|
1082
|
+
deleted.push(id);
|
|
1083
|
+
}
|
|
1084
|
+
if (deleted.length === 0) {
|
|
1085
|
+
return { content: [{ type: 'text', text: `Error: Subtask not found: ${targetIds.join(', ')}` }], isError: true };
|
|
1086
|
+
}
|
|
1087
|
+
writeBoard(filePath, board);
|
|
1088
|
+
if (!useAll && deleted.length === 1 && missing.length === 0) {
|
|
1089
|
+
return { content: [{ type: 'text', text: `Subtask ${deleted[0]} deleted successfully` }] };
|
|
1090
|
+
}
|
|
1091
|
+
return { content: [{ type: 'text', text: JSON.stringify({ deleted, missing, count: deleted.length }, null, 2) }] };
|
|
1035
1092
|
}
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1093
|
+
// ── toggle ─────────────────────────────────────────────────────────────
|
|
1094
|
+
if (action === 'toggle') {
|
|
1095
|
+
if ((0, v2_detect_1.isV2)(filePath)) {
|
|
1096
|
+
const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
|
|
1097
|
+
const found = (0, v2_detect_1.findV2Task)(dirs, task);
|
|
1098
|
+
if (!found) {
|
|
1099
|
+
return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
|
|
1100
|
+
}
|
|
1101
|
+
const t = found.doc.task;
|
|
1102
|
+
if (!t.subtasks || t.subtasks.length === 0) {
|
|
1103
|
+
return { content: [{ type: 'text', text: `Error: Task has no subtasks` }], isError: true };
|
|
1104
|
+
}
|
|
1105
|
+
const targetIds = useAll ? t.subtasks.map(st => st.id) : listParam;
|
|
1106
|
+
if (targetIds.length === 0) {
|
|
1107
|
+
return { content: [{ type: 'text', text: 'Error: subtask or subtasks is required for action=toggle (unless all=true)' }], isError: true };
|
|
1108
|
+
}
|
|
1109
|
+
const targetSet = new Set(targetIds);
|
|
1110
|
+
const updated = [];
|
|
1111
|
+
for (const st of t.subtasks) {
|
|
1112
|
+
if (!targetSet.has(st.id))
|
|
1113
|
+
continue;
|
|
1114
|
+
st.completed = completed !== undefined ? completed : !st.completed;
|
|
1115
|
+
updated.push({ id: st.id, completed: st.completed });
|
|
1116
|
+
}
|
|
1117
|
+
if (updated.length === 0) {
|
|
1118
|
+
return { content: [{ type: 'text', text: `Error: Subtask not found: ${targetIds.join(', ')}` }], isError: true };
|
|
1119
|
+
}
|
|
1120
|
+
t.updatedAt = new Date().toISOString();
|
|
1121
|
+
(0, core_3.writeTaskFile)(found.filePath, t, found.doc.body);
|
|
1122
|
+
if (!useAll && updated.length === 1) {
|
|
1123
|
+
const status = updated[0].completed ? 'completed' : 'incomplete';
|
|
1124
|
+
return { content: [{ type: 'text', text: `Subtask ${updated[0].id} marked as ${status}` }] };
|
|
1125
|
+
}
|
|
1126
|
+
return { content: [{ type: 'text', text: JSON.stringify({ updated, count: updated.length }, null, 2) }] };
|
|
1127
|
+
}
|
|
1128
|
+
const result = readBoard(filePath);
|
|
1129
|
+
if ('error' in result) {
|
|
1130
|
+
return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
|
|
1131
|
+
}
|
|
1132
|
+
let board = result.board;
|
|
1133
|
+
const taskInfo = (0, core_1.findTaskById)(board, task);
|
|
1134
|
+
if (!taskInfo) {
|
|
1135
|
+
return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
|
|
1136
|
+
}
|
|
1137
|
+
const targetIds = useAll ? (taskInfo.task.subtasks || []).map(st => st.id) : listParam;
|
|
1138
|
+
if (targetIds.length === 0) {
|
|
1139
|
+
return { content: [{ type: 'text', text: 'Error: subtask or subtasks is required for action=toggle (unless all=true)' }], isError: true };
|
|
1140
|
+
}
|
|
1141
|
+
const updated = [];
|
|
1142
|
+
if (useAll && completed !== undefined) {
|
|
1143
|
+
const setResult = (0, core_1.setAllSubtasksCompleted)(board, task, completed);
|
|
1144
|
+
if (!setResult.success || !setResult.board) {
|
|
1145
|
+
return { content: [{ type: 'text', text: `Error: ${setResult.error}` }], isError: true };
|
|
1146
|
+
}
|
|
1147
|
+
board = setResult.board;
|
|
1148
|
+
const updatedTask = (0, core_1.findTaskById)(board, task)?.task;
|
|
1149
|
+
for (const st of updatedTask?.subtasks || []) {
|
|
1150
|
+
updated.push({ id: st.id, completed: st.completed });
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
else if (completed !== undefined && targetIds.length > 0) {
|
|
1154
|
+
const setResult = (0, core_1.setSubtasksCompleted)(board, task, targetIds, completed);
|
|
1155
|
+
if (!setResult.success || !setResult.board) {
|
|
1156
|
+
return { content: [{ type: 'text', text: `Error: ${setResult.error}` }], isError: true };
|
|
1157
|
+
}
|
|
1158
|
+
board = setResult.board;
|
|
1159
|
+
const updatedTask = (0, core_1.findTaskById)(board, task)?.task;
|
|
1160
|
+
const targetSet = new Set(targetIds);
|
|
1161
|
+
for (const st of updatedTask?.subtasks || []) {
|
|
1162
|
+
if (targetSet.has(st.id))
|
|
1163
|
+
updated.push({ id: st.id, completed: st.completed });
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
else {
|
|
1167
|
+
for (const id of targetIds) {
|
|
1168
|
+
const toggleResult = (0, core_1.toggleSubtask)(board, task, id);
|
|
1169
|
+
if (!toggleResult.success || !toggleResult.board) {
|
|
1170
|
+
continue;
|
|
1171
|
+
}
|
|
1172
|
+
board = toggleResult.board;
|
|
1173
|
+
const st = (0, core_1.findTaskById)(board, task)?.task.subtasks?.find(entry => entry.id === id);
|
|
1174
|
+
if (st)
|
|
1175
|
+
updated.push({ id: st.id, completed: st.completed });
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
if (updated.length === 0) {
|
|
1179
|
+
return { content: [{ type: 'text', text: `Error: Subtask not found: ${targetIds.join(', ')}` }], isError: true };
|
|
1180
|
+
}
|
|
1181
|
+
writeBoard(filePath, board);
|
|
1182
|
+
if (!useAll && updated.length === 1) {
|
|
1183
|
+
const status = updated[0].completed ? 'completed' : 'incomplete';
|
|
1184
|
+
return { content: [{ type: 'text', text: `Subtask ${updated[0].id} marked as ${status}` }] };
|
|
1185
|
+
}
|
|
1186
|
+
return { content: [{ type: 'text', text: JSON.stringify({ updated, count: updated.length }, null, 2) }] };
|
|
1187
|
+
}
|
|
1188
|
+
// ── update ─────────────────────────────────────────────────────────────
|
|
1189
|
+
if (action === 'update') {
|
|
1190
|
+
const targetIds = listParam;
|
|
1191
|
+
if (targetIds.length === 0) {
|
|
1192
|
+
return { content: [{ type: 'text', text: 'Error: subtask or subtasks is required for action=update' }], isError: true };
|
|
1193
|
+
}
|
|
1194
|
+
const resolvedTitles = resolveUpdateTitles(targetIds);
|
|
1195
|
+
if (!resolvedTitles.ok) {
|
|
1196
|
+
return { content: [{ type: 'text', text: `Error: ${resolvedTitles.error}` }], isError: true };
|
|
1197
|
+
}
|
|
1198
|
+
if ((0, v2_detect_1.isV2)(filePath)) {
|
|
1199
|
+
const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
|
|
1200
|
+
const found = (0, v2_detect_1.findV2Task)(dirs, task);
|
|
1201
|
+
if (!found) {
|
|
1202
|
+
return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
|
|
1203
|
+
}
|
|
1204
|
+
const t = found.doc.task;
|
|
1205
|
+
if (!t.subtasks || t.subtasks.length === 0) {
|
|
1206
|
+
return { content: [{ type: 'text', text: `Error: Task has no subtasks` }], isError: true };
|
|
1207
|
+
}
|
|
1208
|
+
const updates = new Map();
|
|
1209
|
+
targetIds.forEach((id, i) => updates.set(id, resolvedTitles.values[i]));
|
|
1210
|
+
const updated = [];
|
|
1211
|
+
for (const st of t.subtasks) {
|
|
1212
|
+
const nextTitle = updates.get(st.id);
|
|
1213
|
+
if (nextTitle === undefined)
|
|
1214
|
+
continue;
|
|
1215
|
+
st.title = nextTitle;
|
|
1216
|
+
updated.push({ id: st.id, title: st.title });
|
|
1217
|
+
}
|
|
1218
|
+
if (updated.length === 0) {
|
|
1219
|
+
return { content: [{ type: 'text', text: `Error: Subtask not found: ${targetIds.join(', ')}` }], isError: true };
|
|
1220
|
+
}
|
|
1221
|
+
t.updatedAt = new Date().toISOString();
|
|
1222
|
+
(0, core_3.writeTaskFile)(found.filePath, t, found.doc.body);
|
|
1223
|
+
if (updated.length === 1) {
|
|
1224
|
+
return { content: [{ type: 'text', text: `Subtask ${updated[0].id} updated to "${updated[0].title}"` }] };
|
|
1225
|
+
}
|
|
1226
|
+
return { content: [{ type: 'text', text: JSON.stringify({ updated, count: updated.length }, null, 2) }] };
|
|
1227
|
+
}
|
|
1228
|
+
const result = readBoard(filePath);
|
|
1229
|
+
if ('error' in result) {
|
|
1230
|
+
return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
|
|
1231
|
+
}
|
|
1232
|
+
let board = result.board;
|
|
1233
|
+
const updated = [];
|
|
1234
|
+
targetIds.forEach((id, idx) => {
|
|
1235
|
+
const updateResult = (0, core_1.updateSubtask)(board, task, id, resolvedTitles.values[idx]);
|
|
1236
|
+
if (!updateResult.success || !updateResult.board)
|
|
1237
|
+
return;
|
|
1238
|
+
board = updateResult.board;
|
|
1239
|
+
const st = (0, core_1.findTaskById)(board, task)?.task.subtasks?.find(entry => entry.id === id);
|
|
1240
|
+
if (st)
|
|
1241
|
+
updated.push({ id: st.id, title: st.title });
|
|
1242
|
+
});
|
|
1243
|
+
if (updated.length === 0) {
|
|
1244
|
+
return { content: [{ type: 'text', text: `Error: Subtask not found: ${targetIds.join(', ')}` }], isError: true };
|
|
1245
|
+
}
|
|
1246
|
+
writeBoard(filePath, board);
|
|
1247
|
+
if (updated.length === 1) {
|
|
1248
|
+
return { content: [{ type: 'text', text: `Subtask ${updated[0].id} updated to "${updated[0].title}"` }] };
|
|
1249
|
+
}
|
|
1250
|
+
return { content: [{ type: 'text', text: JSON.stringify({ updated, count: updated.length }, null, 2) }] };
|
|
1251
|
+
}
|
|
1252
|
+
return { content: [{ type: 'text', text: `Error: Unknown action: ${action}` }], isError: true };
|
|
1253
|
+
});
|
|
1254
|
+
// Unified contract tool (action-based)
|
|
1255
|
+
server.registerTool('contract', {
|
|
1256
|
+
title: 'Contract',
|
|
1257
|
+
description: [
|
|
1258
|
+
'Unified action-based contract tool.',
|
|
1259
|
+
'action=attach — Attach a new contract to a task (default status=draft; pass ready:true for immediate dispatch)',
|
|
1260
|
+
'action=pickup — Claim a contract (status → in_progress), returns agent context markdown',
|
|
1261
|
+
'action=deliver — Mark contract as delivered (status → delivered)',
|
|
1262
|
+
'action=validate — Validate deliverables + commands (status → done/failed)',
|
|
1263
|
+
'action=graph — Attach contracts to multiple tasks atomically with dependsOn DAG edges (tasks array only)',
|
|
1264
|
+
'action=activate — Flip draft → ready for one task (task param) or all children of a parent (parentId param)',
|
|
1265
|
+
].join('\n'),
|
|
1266
|
+
inputSchema: {
|
|
1267
|
+
action: zod_1.z.enum(['attach', 'pickup', 'deliver', 'validate', 'graph', 'activate']).describe('Contract action'),
|
|
1268
|
+
file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
|
|
1269
|
+
task: zod_1.z.string().optional().describe('Task ID (required for attach, pickup, deliver, validate, and single-task activate)'),
|
|
1270
|
+
parentId: zod_1.z.string().optional().describe('For activate: activate all draft contracts whose parentId matches this value'),
|
|
1271
|
+
// attach fields
|
|
1272
|
+
ready: zod_1.z.boolean().optional().describe('attach only: when true, status=ready instead of draft'),
|
|
1273
|
+
deliverables: zod_1.z.array(zod_1.z.string()).optional().describe('attach only: type:path:description'),
|
|
1274
|
+
validation_commands: zod_1.z.array(zod_1.z.string()).optional().describe('attach only: validation shell commands'),
|
|
1275
|
+
constraints: zod_1.z.array(zod_1.z.string()).optional().describe('attach only: constraint strings'),
|
|
1276
|
+
tasks: zod_1.z.array(zod_1.z.object({
|
|
1277
|
+
task: zod_1.z.string(),
|
|
1278
|
+
deliverables: zod_1.z.array(zod_1.z.object({
|
|
1279
|
+
type: zod_1.z.enum(['file', 'test', 'docs', 'design', 'research']),
|
|
1280
|
+
path: zod_1.z.string(),
|
|
1281
|
+
description: zod_1.z.string().optional(),
|
|
1282
|
+
})).optional(),
|
|
1283
|
+
validation_commands: zod_1.z.array(zod_1.z.string()).optional(),
|
|
1284
|
+
constraints: zod_1.z.array(zod_1.z.string()).optional(),
|
|
1285
|
+
dependsOn: zod_1.z.array(zod_1.z.string()).optional(),
|
|
1286
|
+
})).optional().describe('graph only: array of contract graph task specs'),
|
|
1287
|
+
activate: zod_1.z.boolean().optional().describe('graph only: when true, attached contracts start in ready instead of draft'),
|
|
1288
|
+
}
|
|
1289
|
+
}, async ({ action, file, task, parentId, ready: attachReady, deliverables, validation_commands, constraints, tasks, activate }) => {
|
|
1290
|
+
const filePath = file || defaultFile;
|
|
1291
|
+
// ── attach ─────────────────────────────────────────────────────────────
|
|
1292
|
+
if (action === 'attach') {
|
|
1293
|
+
if (!task) {
|
|
1294
|
+
return { content: [{ type: 'text', text: 'Error: task is required for action=attach' }], isError: true };
|
|
1295
|
+
}
|
|
1296
|
+
if ((0, v2_detect_1.isV2)(filePath)) {
|
|
1297
|
+
const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
|
|
1298
|
+
const found = (0, v2_detect_1.findV2Task)(dirs, task);
|
|
1299
|
+
if (!found) {
|
|
1300
|
+
return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
|
|
1301
|
+
}
|
|
1302
|
+
try {
|
|
1303
|
+
const contract = (0, contractSpec_1.buildContract)({
|
|
1304
|
+
deliverableSpecs: deliverables,
|
|
1305
|
+
validationCommands: validation_commands,
|
|
1306
|
+
constraints,
|
|
1307
|
+
status: attachReady ? 'ready' : 'draft',
|
|
1308
|
+
});
|
|
1309
|
+
found.doc.task.contract = contract;
|
|
1310
|
+
found.doc.task.updatedAt = new Date().toISOString();
|
|
1311
|
+
(0, core_3.writeTaskFile)(found.filePath, found.doc.task, found.doc.body);
|
|
1312
|
+
return { content: [{ type: 'text', text: `Contract attached (${contract.status}): ${task}` }] };
|
|
1313
|
+
}
|
|
1314
|
+
catch (e) {
|
|
1315
|
+
return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
const readResult = readBoard(filePath);
|
|
1319
|
+
if ('error' in readResult) {
|
|
1320
|
+
return { content: [{ type: 'text', text: `Error: ${readResult.error}` }], isError: true };
|
|
1321
|
+
}
|
|
1322
|
+
const taskInfo = (0, core_1.findTaskById)(readResult.board, task);
|
|
1323
|
+
if (!taskInfo) {
|
|
1324
|
+
return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
|
|
1325
|
+
}
|
|
1326
|
+
try {
|
|
1327
|
+
const contract = (0, contractSpec_1.buildContract)({
|
|
1328
|
+
deliverableSpecs: deliverables,
|
|
1329
|
+
validationCommands: validation_commands,
|
|
1330
|
+
constraints,
|
|
1331
|
+
status: attachReady ? 'ready' : 'draft',
|
|
1332
|
+
});
|
|
1333
|
+
const contractResult = (0, core_1.setTaskContract)(readResult.board, task, contract);
|
|
1334
|
+
if (!contractResult.success || !contractResult.board) {
|
|
1335
|
+
return { content: [{ type: 'text', text: `Error: ${contractResult.error || 'Failed to attach contract'}` }], isError: true };
|
|
1336
|
+
}
|
|
1337
|
+
writeBoard(filePath, contractResult.board);
|
|
1338
|
+
return { content: [{ type: 'text', text: `Contract attached (${contract.status}): ${task}` }] };
|
|
1339
|
+
}
|
|
1340
|
+
catch (e) {
|
|
1341
|
+
return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
// ── pickup ─────────────────────────────────────────────────────────────
|
|
1345
|
+
if (action === 'pickup') {
|
|
1346
|
+
if (!task) {
|
|
1347
|
+
return { content: [{ type: 'text', text: 'Error: task is required for action=pickup' }], isError: true };
|
|
1348
|
+
}
|
|
1349
|
+
const result = (0, contractRunner_1.pickupContract)({ filePath, taskId: task });
|
|
1350
|
+
if ('error' in result) {
|
|
1351
|
+
return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
|
|
1352
|
+
}
|
|
1353
|
+
return { content: [{ type: 'text', text: result.markdown }] };
|
|
1354
|
+
}
|
|
1355
|
+
// ── deliver ────────────────────────────────────────────────────────────
|
|
1356
|
+
if (action === 'deliver') {
|
|
1357
|
+
if (!task) {
|
|
1358
|
+
return { content: [{ type: 'text', text: 'Error: task is required for action=deliver' }], isError: true };
|
|
1359
|
+
}
|
|
1360
|
+
const result = (0, contractRunner_1.deliverContract)({ filePath, taskId: task });
|
|
1361
|
+
if ('error' in result) {
|
|
1362
|
+
return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
|
|
1363
|
+
}
|
|
1364
|
+
return { content: [{ type: 'text', text: `Contract delivered: ${task}` }] };
|
|
1041
1365
|
}
|
|
1042
|
-
//
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
const
|
|
1048
|
-
if (
|
|
1049
|
-
return { content: [{ type: 'text', text: `Error: ${
|
|
1050
|
-
}
|
|
1051
|
-
const
|
|
1366
|
+
// ── validate ───────────────────────────────────────────────────────────
|
|
1367
|
+
if (action === 'validate') {
|
|
1368
|
+
if (!task) {
|
|
1369
|
+
return { content: [{ type: 'text', text: 'Error: task is required for action=validate' }], isError: true };
|
|
1370
|
+
}
|
|
1371
|
+
const result = (0, contractRunner_1.validateContract)({ filePath, taskId: task });
|
|
1372
|
+
if ('error' in result) {
|
|
1373
|
+
return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
|
|
1374
|
+
}
|
|
1375
|
+
const output = {
|
|
1376
|
+
ok: result.ok,
|
|
1377
|
+
status: result.ok ? 'done' : 'failed',
|
|
1378
|
+
deliverables: result.deliverableChecks,
|
|
1379
|
+
commands: result.commandResults,
|
|
1380
|
+
warnings: result.warnings,
|
|
1381
|
+
};
|
|
1052
1382
|
return {
|
|
1053
|
-
content: [{ type: 'text', text:
|
|
1383
|
+
content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
|
|
1384
|
+
isError: !result.ok,
|
|
1054
1385
|
};
|
|
1055
1386
|
}
|
|
1056
|
-
//
|
|
1057
|
-
if (
|
|
1058
|
-
|
|
1059
|
-
|
|
1387
|
+
// ── graph ──────────────────────────────────────────────────────────────
|
|
1388
|
+
if (action === 'graph') {
|
|
1389
|
+
if (!tasks || tasks.length === 0) {
|
|
1390
|
+
return { content: [{ type: 'text', text: 'Error: tasks is required for action=graph and must be a non-empty array' }], isError: true };
|
|
1391
|
+
}
|
|
1392
|
+
try {
|
|
1393
|
+
const result = (0, contract_1.executeContractGraphMcpAction)({
|
|
1394
|
+
file: filePath,
|
|
1395
|
+
tasks,
|
|
1396
|
+
activate,
|
|
1397
|
+
});
|
|
1060
1398
|
return {
|
|
1061
|
-
content: [{
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1399
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
1400
|
+
attached: result.attached,
|
|
1401
|
+
count: result.count,
|
|
1402
|
+
order: result.order,
|
|
1403
|
+
graph: result.graph,
|
|
1404
|
+
}, null, 2) }],
|
|
1066
1405
|
};
|
|
1067
1406
|
}
|
|
1068
|
-
|
|
1069
|
-
const config = (0, config_1.getArchiveConfig)();
|
|
1070
|
-
if (!config.github?.owner || !config.github?.repo) {
|
|
1407
|
+
catch (error) {
|
|
1071
1408
|
return {
|
|
1072
|
-
content: [{
|
|
1073
|
-
|
|
1074
|
-
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>\n\nOr fall back to local archive:\n Use destination: "local"`
|
|
1075
|
-
}],
|
|
1076
|
-
isError: true
|
|
1409
|
+
content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
|
|
1410
|
+
isError: true,
|
|
1077
1411
|
};
|
|
1078
1412
|
}
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1413
|
+
}
|
|
1414
|
+
// ── activate ───────────────────────────────────────────────────────────
|
|
1415
|
+
if (action === 'activate') {
|
|
1416
|
+
if (!task && !parentId) {
|
|
1417
|
+
return { content: [{ type: 'text', text: 'Error: task or parentId is required for action=activate' }], isError: true };
|
|
1418
|
+
}
|
|
1419
|
+
const activated = [];
|
|
1420
|
+
if ((0, v2_detect_1.isV2)(filePath)) {
|
|
1421
|
+
const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
|
|
1422
|
+
if (task) {
|
|
1423
|
+
const found = (0, v2_detect_1.findV2Task)(dirs, task, false);
|
|
1424
|
+
if (!found || found.isLog) {
|
|
1425
|
+
return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
|
|
1426
|
+
}
|
|
1427
|
+
if (!found.doc.task.contract) {
|
|
1428
|
+
return { content: [{ type: 'text', text: `Error: Task ${task} has no contract` }], isError: true };
|
|
1429
|
+
}
|
|
1430
|
+
if (found.doc.task.contract.status !== 'draft') {
|
|
1431
|
+
return { content: [{ type: 'text', text: `Error: Contract is not in draft status (current: ${found.doc.task.contract.status})` }], isError: true };
|
|
1432
|
+
}
|
|
1433
|
+
const readyAt = new Date().toISOString();
|
|
1434
|
+
found.doc.task.contract = {
|
|
1435
|
+
...found.doc.task.contract,
|
|
1436
|
+
status: 'ready',
|
|
1437
|
+
metrics: {
|
|
1438
|
+
...(found.doc.task.contract.metrics ?? {}),
|
|
1439
|
+
readyAt,
|
|
1440
|
+
},
|
|
1441
|
+
};
|
|
1442
|
+
found.doc.task.updatedAt = readyAt;
|
|
1443
|
+
(0, core_3.writeTaskFile)(found.filePath, found.doc.task, found.doc.body);
|
|
1444
|
+
activated.push(task);
|
|
1445
|
+
}
|
|
1446
|
+
else {
|
|
1447
|
+
// Bulk by parentId
|
|
1448
|
+
const allTasks = (0, core_3.readTasksDir)(dirs.boardDir);
|
|
1449
|
+
for (const doc of allTasks) {
|
|
1450
|
+
const t = doc.task;
|
|
1451
|
+
if (t.parentId !== parentId)
|
|
1452
|
+
continue;
|
|
1453
|
+
if (!t.contract || t.contract.status !== 'draft')
|
|
1454
|
+
continue;
|
|
1455
|
+
const readyAt = new Date().toISOString();
|
|
1456
|
+
t.contract = {
|
|
1457
|
+
...t.contract,
|
|
1458
|
+
status: 'ready',
|
|
1459
|
+
metrics: {
|
|
1460
|
+
...(t.contract.metrics ?? {}),
|
|
1461
|
+
readyAt,
|
|
1462
|
+
},
|
|
1463
|
+
};
|
|
1464
|
+
t.updatedAt = readyAt;
|
|
1465
|
+
(0, core_3.writeTaskFile)(path.join(dirs.boardDir, (0, core_3.taskFileName)(t.id)), t, doc.body);
|
|
1466
|
+
activated.push(t.id);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
const output = { activated, count: activated.length };
|
|
1470
|
+
return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
|
|
1471
|
+
}
|
|
1472
|
+
// V1
|
|
1473
|
+
const readResult = readBoard(filePath);
|
|
1474
|
+
if ('error' in readResult) {
|
|
1475
|
+
return { content: [{ type: 'text', text: `Error: ${readResult.error}` }], isError: true };
|
|
1476
|
+
}
|
|
1477
|
+
let board = readResult.board;
|
|
1478
|
+
if (task) {
|
|
1479
|
+
const taskInfo = (0, core_1.findTaskById)(board, task);
|
|
1480
|
+
if (!taskInfo) {
|
|
1481
|
+
return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
|
|
1482
|
+
}
|
|
1483
|
+
if (!taskInfo.task.contract) {
|
|
1484
|
+
return { content: [{ type: 'text', text: `Error: Task ${task} has no contract` }], isError: true };
|
|
1485
|
+
}
|
|
1486
|
+
if (taskInfo.task.contract.status !== 'draft') {
|
|
1487
|
+
return { content: [{ type: 'text', text: `Error: Contract is not in draft status (current: ${taskInfo.task.contract.status})` }], isError: true };
|
|
1488
|
+
}
|
|
1489
|
+
const readyAt = new Date().toISOString();
|
|
1490
|
+
const updatedContract = {
|
|
1491
|
+
...taskInfo.task.contract,
|
|
1492
|
+
status: 'ready',
|
|
1493
|
+
metrics: {
|
|
1494
|
+
...(taskInfo.task.contract.metrics ?? {}),
|
|
1495
|
+
readyAt,
|
|
1496
|
+
},
|
|
1100
1497
|
};
|
|
1498
|
+
const contractResult = (0, core_1.setTaskContract)(board, task, updatedContract);
|
|
1499
|
+
if (!contractResult.success || !contractResult.board) {
|
|
1500
|
+
return { content: [{ type: 'text', text: `Error: ${contractResult.error || 'Failed to activate contract'}` }], isError: true };
|
|
1501
|
+
}
|
|
1502
|
+
board = contractResult.board;
|
|
1503
|
+
activated.push(task);
|
|
1101
1504
|
}
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1505
|
+
else {
|
|
1506
|
+
// Bulk by parentId
|
|
1507
|
+
for (const col of board.columns) {
|
|
1508
|
+
for (const t of col.tasks) {
|
|
1509
|
+
const taskAny = t;
|
|
1510
|
+
if (taskAny.parentId !== parentId)
|
|
1511
|
+
continue;
|
|
1512
|
+
if (!t.contract || t.contract.status !== 'draft')
|
|
1513
|
+
continue;
|
|
1514
|
+
const readyAt = new Date().toISOString();
|
|
1515
|
+
const updatedContract = {
|
|
1516
|
+
...t.contract,
|
|
1517
|
+
status: 'ready',
|
|
1518
|
+
metrics: {
|
|
1519
|
+
...(t.contract.metrics ?? {}),
|
|
1520
|
+
readyAt,
|
|
1521
|
+
},
|
|
1522
|
+
};
|
|
1523
|
+
const contractResult = (0, core_1.setTaskContract)(board, t.id, updatedContract);
|
|
1524
|
+
if (contractResult.success && contractResult.board) {
|
|
1525
|
+
board = contractResult.board;
|
|
1526
|
+
activated.push(t.id);
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1106
1530
|
}
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
text: `Task ${task} archived to GitHub Issue #${ghResult.issueNumber} (closed)\n\nView: ${ghResult.issueUrl}`
|
|
1111
|
-
}]
|
|
1112
|
-
};
|
|
1531
|
+
writeBoard(filePath, board);
|
|
1532
|
+
const output = { activated, count: activated.length };
|
|
1533
|
+
return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
|
|
1113
1534
|
}
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1535
|
+
return { content: [{ type: 'text', text: `Error: Unknown action: ${action}` }], isError: true };
|
|
1536
|
+
});
|
|
1537
|
+
// Task complete tool (also supports archive destinations)
|
|
1538
|
+
server.registerTool('task_complete', {
|
|
1539
|
+
title: 'Complete Task',
|
|
1540
|
+
description: 'Complete a task or archive it to local/GitHub/Linear destination',
|
|
1541
|
+
inputSchema: {
|
|
1542
|
+
file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
|
|
1543
|
+
task: zod_1.z.string().describe('Task ID to complete'),
|
|
1544
|
+
destination: zod_1.z.enum(['local', 'github', 'linear']).optional().describe('Optional archive destination. If omitted, performs normal completion flow.'),
|
|
1545
|
+
}
|
|
1546
|
+
}, async ({ file, task, destination }) => {
|
|
1547
|
+
const filePath = file || defaultFile;
|
|
1548
|
+
try {
|
|
1549
|
+
// Default behavior: normal complete flow (v2 -> logs, v1 -> done column)
|
|
1550
|
+
if (!destination) {
|
|
1551
|
+
const { completeCommand } = await Promise.resolve().then(() => __importStar(require('./complete')));
|
|
1552
|
+
const result = completeCommand({ file: filePath, task }, { log: () => { }, warn: () => { }, error: () => { }, info: () => { } });
|
|
1553
|
+
return {
|
|
1554
|
+
content: [{ type: 'text', text: `Task ${task} completed at ${result.completedAt}` }]
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1557
|
+
// Local archive keeps legacy archive_task behavior for v1
|
|
1558
|
+
if (destination === 'local') {
|
|
1559
|
+
if ((0, v2_detect_1.isV2)(filePath)) {
|
|
1560
|
+
const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
|
|
1561
|
+
const found = (0, v2_detect_1.findV2Task)(dirs, task);
|
|
1562
|
+
if (!found) {
|
|
1563
|
+
return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
|
|
1564
|
+
}
|
|
1565
|
+
const logPath = path.join(dirs.logsDir, (0, core_3.taskFileName)(task));
|
|
1566
|
+
found.doc.task.completedAt = found.doc.task.completedAt || new Date().toISOString();
|
|
1567
|
+
delete found.doc.task.column;
|
|
1568
|
+
delete found.doc.task.position;
|
|
1569
|
+
(0, core_3.writeTaskFile)(logPath, found.doc.task, found.doc.body);
|
|
1570
|
+
fs.unlinkSync(found.filePath);
|
|
1571
|
+
return { content: [{ type: 'text', text: `Task ${task} archived to logs/` }] };
|
|
1572
|
+
}
|
|
1573
|
+
const readResult = readBoard(filePath);
|
|
1574
|
+
if ('error' in readResult) {
|
|
1575
|
+
return { content: [{ type: 'text', text: `Error: ${readResult.error}` }], isError: true };
|
|
1576
|
+
}
|
|
1577
|
+
const taskInfo = (0, core_1.findTaskById)(readResult.board, task);
|
|
1578
|
+
if (!taskInfo) {
|
|
1579
|
+
return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
|
|
1580
|
+
}
|
|
1581
|
+
const archiveResult = (0, archive_1.archiveTaskToFile)(filePath, readResult.board, taskInfo.column.id, task);
|
|
1582
|
+
if (!archiveResult.success) {
|
|
1583
|
+
return { content: [{ type: 'text', text: `Error: ${archiveResult.error}` }], isError: true };
|
|
1584
|
+
}
|
|
1118
1585
|
return {
|
|
1119
|
-
content: [{
|
|
1120
|
-
type: 'text',
|
|
1121
|
-
text: `Error: Not authenticated with Linear.\n\nTo authenticate, run:\n npx @brainfile/cli auth linear --token <api-key>\n\nGet your API key from: https://linear.app/settings/api\n\nOr fall back to local archive:\n Use destination: "local"`
|
|
1122
|
-
}],
|
|
1123
|
-
isError: true
|
|
1586
|
+
content: [{ type: 'text', text: `Task ${task} archived to ${path.basename((0, archive_1.getArchivePath)(filePath))}` }]
|
|
1124
1587
|
};
|
|
1125
1588
|
}
|
|
1126
|
-
//
|
|
1589
|
+
// External archive destinations: legacy archive behavior
|
|
1590
|
+
if ((0, v2_detect_1.isV2)(filePath)) {
|
|
1591
|
+
const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
|
|
1592
|
+
const found = (0, v2_detect_1.findV2Task)(dirs, task);
|
|
1593
|
+
if (!found) {
|
|
1594
|
+
return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
|
|
1595
|
+
}
|
|
1596
|
+
const board = (0, v2_detect_1.readV2BoardConfig)(filePath);
|
|
1597
|
+
if (destination === 'github') {
|
|
1598
|
+
if (!(await (0, github_auth_1.isGitHubAuthenticated)())) {
|
|
1599
|
+
return { content: [{ type: 'text', text: 'Error: Not authenticated with GitHub.' }], isError: true };
|
|
1600
|
+
}
|
|
1601
|
+
const config = (0, config_1.getArchiveConfig)();
|
|
1602
|
+
if (!config.github?.owner || !config.github?.repo) {
|
|
1603
|
+
return { content: [{ type: 'text', text: 'Error: GitHub repository not configured.' }], isError: true };
|
|
1604
|
+
}
|
|
1605
|
+
const payload = (0, core_2.formatTaskForGitHub)(found.doc.task, {
|
|
1606
|
+
includeMeta: true,
|
|
1607
|
+
includeSubtasks: true,
|
|
1608
|
+
includeRelatedFiles: true,
|
|
1609
|
+
boardTitle: board.title,
|
|
1610
|
+
fromColumn: found.doc.task.column || 'unknown',
|
|
1611
|
+
extraLabels: config.github.labels,
|
|
1612
|
+
});
|
|
1613
|
+
const ghResult = await (0, github_auth_1.createGitHubIssue)({
|
|
1614
|
+
owner: config.github.owner,
|
|
1615
|
+
repo: config.github.repo,
|
|
1616
|
+
title: payload.title,
|
|
1617
|
+
body: payload.body,
|
|
1618
|
+
labels: payload.labels,
|
|
1619
|
+
state: 'closed',
|
|
1620
|
+
});
|
|
1621
|
+
if (!ghResult.success) {
|
|
1622
|
+
return { content: [{ type: 'text', text: `Error creating GitHub issue: ${ghResult.error}` }], isError: true };
|
|
1623
|
+
}
|
|
1624
|
+
fs.unlinkSync(found.filePath);
|
|
1625
|
+
return { content: [{ type: 'text', text: `Task ${task} archived to GitHub Issue #${ghResult.issueNumber} (closed)\n\nView: ${ghResult.issueUrl}` }] };
|
|
1626
|
+
}
|
|
1627
|
+
if (!(await (0, linear_auth_1.isLinearAuthenticated)())) {
|
|
1628
|
+
return { content: [{ type: 'text', text: 'Error: Not authenticated with Linear.' }], isError: true };
|
|
1629
|
+
}
|
|
1630
|
+
const config = (0, config_1.getArchiveConfig)();
|
|
1631
|
+
let teamId = config.linear?.teamId;
|
|
1632
|
+
if (!teamId) {
|
|
1633
|
+
const teams = await (0, linear_auth_1.getLinearTeams)();
|
|
1634
|
+
if (teams.length === 0) {
|
|
1635
|
+
return { content: [{ type: 'text', text: 'Error: No Linear teams found.' }], isError: true };
|
|
1636
|
+
}
|
|
1637
|
+
if (teams.length === 1) {
|
|
1638
|
+
teamId = teams[0].id;
|
|
1639
|
+
}
|
|
1640
|
+
else {
|
|
1641
|
+
const teamList = teams.map(t => ` ${t.key}: ${t.name} (${t.id})`).join('\n');
|
|
1642
|
+
return { content: [{ type: 'text', text: `Error: Multiple Linear teams found. Please configure a default.\n\nAvailable teams:\n${teamList}` }], isError: true };
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
const payload = (0, core_2.formatTaskForLinear)(found.doc.task, {
|
|
1646
|
+
includeMeta: true,
|
|
1647
|
+
includeSubtasks: true,
|
|
1648
|
+
includeRelatedFiles: true,
|
|
1649
|
+
boardTitle: board.title,
|
|
1650
|
+
fromColumn: found.doc.task.column || 'unknown',
|
|
1651
|
+
stateName: 'Done',
|
|
1652
|
+
});
|
|
1653
|
+
const linearResult = await (0, linear_auth_1.createLinearIssue)({
|
|
1654
|
+
teamId,
|
|
1655
|
+
title: payload.title,
|
|
1656
|
+
description: payload.description,
|
|
1657
|
+
priority: payload.priority,
|
|
1658
|
+
labelNames: payload.labelNames,
|
|
1659
|
+
stateName: 'Done',
|
|
1660
|
+
});
|
|
1661
|
+
if (!linearResult.success) {
|
|
1662
|
+
return { content: [{ type: 'text', text: `Error creating Linear issue: ${linearResult.error}` }], isError: true };
|
|
1663
|
+
}
|
|
1664
|
+
fs.unlinkSync(found.filePath);
|
|
1665
|
+
return { content: [{ type: 'text', text: `Task ${task} archived to Linear Issue ${linearResult.issueId} (Done)\n\nView: ${linearResult.issueUrl}` }] };
|
|
1666
|
+
}
|
|
1667
|
+
const readResult = readBoard(filePath);
|
|
1668
|
+
if ('error' in readResult) {
|
|
1669
|
+
return { content: [{ type: 'text', text: `Error: ${readResult.error}` }], isError: true };
|
|
1670
|
+
}
|
|
1671
|
+
const { board } = readResult;
|
|
1672
|
+
const taskInfo = (0, core_1.findTaskById)(board, task);
|
|
1673
|
+
if (!taskInfo) {
|
|
1674
|
+
return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
|
|
1675
|
+
}
|
|
1676
|
+
if (destination === 'github') {
|
|
1677
|
+
if (!(await (0, github_auth_1.isGitHubAuthenticated)())) {
|
|
1678
|
+
return { content: [{ type: 'text', text: 'Error: Not authenticated with GitHub.' }], isError: true };
|
|
1679
|
+
}
|
|
1680
|
+
const config = (0, config_1.getArchiveConfig)();
|
|
1681
|
+
if (!config.github?.owner || !config.github?.repo) {
|
|
1682
|
+
return { content: [{ type: 'text', text: 'Error: GitHub repository not configured.' }], isError: true };
|
|
1683
|
+
}
|
|
1684
|
+
const payload = (0, core_2.formatTaskForGitHub)(taskInfo.task, {
|
|
1685
|
+
includeMeta: true,
|
|
1686
|
+
includeSubtasks: true,
|
|
1687
|
+
includeRelatedFiles: true,
|
|
1688
|
+
boardTitle: board.title,
|
|
1689
|
+
fromColumn: taskInfo.column.title,
|
|
1690
|
+
extraLabels: config.github.labels,
|
|
1691
|
+
});
|
|
1692
|
+
const ghResult = await (0, github_auth_1.createGitHubIssue)({
|
|
1693
|
+
owner: config.github.owner,
|
|
1694
|
+
repo: config.github.repo,
|
|
1695
|
+
title: payload.title,
|
|
1696
|
+
body: payload.body,
|
|
1697
|
+
labels: payload.labels,
|
|
1698
|
+
state: 'closed',
|
|
1699
|
+
});
|
|
1700
|
+
if (!ghResult.success) {
|
|
1701
|
+
return { content: [{ type: 'text', text: `Error creating GitHub issue: ${ghResult.error}` }], isError: true };
|
|
1702
|
+
}
|
|
1703
|
+
const deleteResult = (0, core_1.deleteTask)(board, taskInfo.column.id, task);
|
|
1704
|
+
if (deleteResult.success)
|
|
1705
|
+
writeBoard(filePath, deleteResult.board);
|
|
1706
|
+
return { content: [{ type: 'text', text: `Task ${task} archived to GitHub Issue #${ghResult.issueNumber} (closed)\n\nView: ${ghResult.issueUrl}` }] };
|
|
1707
|
+
}
|
|
1708
|
+
if (!(await (0, linear_auth_1.isLinearAuthenticated)())) {
|
|
1709
|
+
return { content: [{ type: 'text', text: 'Error: Not authenticated with Linear.' }], isError: true };
|
|
1710
|
+
}
|
|
1127
1711
|
const config = (0, config_1.getArchiveConfig)();
|
|
1128
1712
|
let teamId = config.linear?.teamId;
|
|
1129
1713
|
if (!teamId) {
|
|
1130
1714
|
const teams = await (0, linear_auth_1.getLinearTeams)();
|
|
1131
1715
|
if (teams.length === 0) {
|
|
1132
|
-
return {
|
|
1133
|
-
content: [{
|
|
1134
|
-
type: 'text',
|
|
1135
|
-
text: `Error: No Linear teams found.\n\nVerify your authentication:\n npx @brainfile/cli auth status`
|
|
1136
|
-
}],
|
|
1137
|
-
isError: true
|
|
1138
|
-
};
|
|
1716
|
+
return { content: [{ type: 'text', text: 'Error: No Linear teams found.' }], isError: true };
|
|
1139
1717
|
}
|
|
1140
1718
|
if (teams.length === 1) {
|
|
1141
1719
|
teamId = teams[0].id;
|
|
1142
1720
|
}
|
|
1143
1721
|
else {
|
|
1144
1722
|
const teamList = teams.map(t => ` ${t.key}: ${t.name} (${t.id})`).join('\n');
|
|
1145
|
-
return {
|
|
1146
|
-
content: [{
|
|
1147
|
-
type: 'text',
|
|
1148
|
-
text: `Error: Multiple Linear teams found. Please configure a default.\n\nAvailable teams:\n${teamList}\n\nTo configure, run:\n npx @brainfile/cli config set archive.linear.teamId <team-id>\n\nOr fall back to local archive:\n Use destination: "local"`
|
|
1149
|
-
}],
|
|
1150
|
-
isError: true
|
|
1151
|
-
};
|
|
1723
|
+
return { content: [{ type: 'text', text: `Error: Multiple Linear teams found. Please configure a default.\n\nAvailable teams:\n${teamList}` }], isError: true };
|
|
1152
1724
|
}
|
|
1153
1725
|
}
|
|
1154
|
-
// Format and create Linear issue
|
|
1155
1726
|
const payload = (0, core_2.formatTaskForLinear)(taskInfo.task, {
|
|
1156
1727
|
includeMeta: true,
|
|
1157
1728
|
includeSubtasks: true,
|
|
@@ -1169,1314 +1740,17 @@ async function mcpCommand(options) {
|
|
|
1169
1740
|
stateName: 'Done',
|
|
1170
1741
|
});
|
|
1171
1742
|
if (!linearResult.success) {
|
|
1172
|
-
return {
|
|
1173
|
-
content: [{ type: 'text', text: `Error creating Linear issue: ${linearResult.error}` }],
|
|
1174
|
-
isError: true
|
|
1175
|
-
};
|
|
1743
|
+
return { content: [{ type: 'text', text: `Error creating Linear issue: ${linearResult.error}` }], isError: true };
|
|
1176
1744
|
}
|
|
1177
|
-
// Remove task from board
|
|
1178
1745
|
const deleteResult = (0, core_1.deleteTask)(board, taskInfo.column.id, task);
|
|
1179
|
-
if (deleteResult.success)
|
|
1746
|
+
if (deleteResult.success)
|
|
1180
1747
|
writeBoard(filePath, deleteResult.board);
|
|
1181
|
-
}
|
|
1182
|
-
return {
|
|
1183
|
-
content: [{
|
|
1184
|
-
type: 'text',
|
|
1185
|
-
text: `Task ${task} archived to Linear Issue ${linearResult.issueId} (Done)\n\nView: ${linearResult.issueUrl}`
|
|
1186
|
-
}]
|
|
1187
|
-
};
|
|
1188
|
-
}
|
|
1189
|
-
return {
|
|
1190
|
-
content: [{ type: 'text', text: `Error: Unknown destination: ${effectiveDestination}` }],
|
|
1191
|
-
isError: true
|
|
1192
|
-
};
|
|
1193
|
-
});
|
|
1194
|
-
// Restore task tool
|
|
1195
|
-
server.registerTool('restore_task', {
|
|
1196
|
-
title: 'Restore Task',
|
|
1197
|
-
description: 'Restore a task from the archive to a column',
|
|
1198
|
-
inputSchema: {
|
|
1199
|
-
file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
|
|
1200
|
-
task: zod_1.z.string().describe('Task ID to restore'),
|
|
1201
|
-
column: zod_1.z.string().describe('Target column ID or name')
|
|
1202
|
-
}
|
|
1203
|
-
}, async ({ file, task, column }) => {
|
|
1204
|
-
const filePath = file || defaultFile;
|
|
1205
|
-
// V2: restore means move from logs/ to board/
|
|
1206
|
-
if ((0, v2_detect_1.isV2)(filePath)) {
|
|
1207
|
-
const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
|
|
1208
|
-
const board = (0, v2_detect_1.readV2BoardConfig)(filePath);
|
|
1209
|
-
let targetColumn = board.columns.find(c => c.id === column);
|
|
1210
|
-
if (!targetColumn)
|
|
1211
|
-
targetColumn = board.columns.find(c => c.title.toLowerCase() === column.toLowerCase());
|
|
1212
|
-
if (!targetColumn) {
|
|
1213
|
-
return { content: [{ type: 'text', text: `Error: Column not found: ${column}` }], isError: true };
|
|
1214
|
-
}
|
|
1215
|
-
// Look in logs
|
|
1216
|
-
const logPath = path.join(dirs.logsDir, (0, core_3.taskFileName)(task));
|
|
1217
|
-
const doc = (0, core_3.readTaskFile)(logPath);
|
|
1218
|
-
if (!doc) {
|
|
1219
|
-
return { content: [{ type: 'text', text: `Error: Task not found in logs: ${task}` }], isError: true };
|
|
1220
|
-
}
|
|
1221
|
-
// Move to board/ with column info
|
|
1222
|
-
const targetTasks = (0, core_3.readTasksDir)(dirs.boardDir).filter(t => t.task.column === targetColumn.id);
|
|
1223
|
-
doc.task.column = targetColumn.id;
|
|
1224
|
-
doc.task.position = targetTasks.length;
|
|
1225
|
-
delete doc.task.completedAt;
|
|
1226
|
-
doc.task.updatedAt = new Date().toISOString();
|
|
1227
|
-
const taskPath = path.join(dirs.boardDir, (0, core_3.taskFileName)(task));
|
|
1228
|
-
(0, core_3.writeTaskFile)(taskPath, doc.task, doc.body);
|
|
1229
|
-
fs.unlinkSync(logPath);
|
|
1230
|
-
return { content: [{ type: 'text', text: `Task ${task} restored to "${targetColumn.title}"` }] };
|
|
1231
|
-
}
|
|
1232
|
-
// V1: use board
|
|
1233
|
-
const result = readBoard(filePath);
|
|
1234
|
-
if ('error' in result) {
|
|
1235
|
-
return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
|
|
1236
|
-
}
|
|
1237
|
-
let { board } = result;
|
|
1238
|
-
// Find target column
|
|
1239
|
-
let targetColumn = (0, core_1.findColumnById)(board, column);
|
|
1240
|
-
if (!targetColumn) {
|
|
1241
|
-
targetColumn = (0, core_1.findColumnByName)(board, column);
|
|
1242
|
-
}
|
|
1243
|
-
if (!targetColumn) {
|
|
1244
|
-
return { content: [{ type: 'text', text: `Error: Column not found: ${column}` }], isError: true };
|
|
1245
|
-
}
|
|
1246
|
-
// Restore from separate archive file
|
|
1247
|
-
const restoreResult = (0, archive_1.restoreFromArchive)(filePath, task, targetColumn.id);
|
|
1248
|
-
if (!restoreResult.success) {
|
|
1249
|
-
// Provide helpful error if archive is empty
|
|
1250
|
-
const { tasks } = (0, archive_1.loadArchivedTasks)(filePath);
|
|
1251
|
-
if (tasks.length === 0) {
|
|
1252
|
-
return {
|
|
1253
|
-
content: [{ type: 'text', text: `Error: Archive is empty (${path.basename((0, archive_1.getArchivePath)(filePath))})` }],
|
|
1254
|
-
isError: true
|
|
1255
|
-
};
|
|
1256
|
-
}
|
|
1257
|
-
return { content: [{ type: 'text', text: `Error: ${restoreResult.error}` }], isError: true };
|
|
1258
|
-
}
|
|
1259
|
-
return {
|
|
1260
|
-
content: [{ type: 'text', text: `Task ${task} restored to "${targetColumn.title}"` }]
|
|
1261
|
-
};
|
|
1262
|
-
});
|
|
1263
|
-
// Add subtask tool
|
|
1264
|
-
server.registerTool('add_subtask', {
|
|
1265
|
-
title: 'Add Subtask',
|
|
1266
|
-
description: 'Add a subtask to a task',
|
|
1267
|
-
inputSchema: {
|
|
1268
|
-
file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
|
|
1269
|
-
task: zod_1.z.string().describe('Parent task ID'),
|
|
1270
|
-
title: zod_1.z.string().describe('Subtask title')
|
|
1271
|
-
}
|
|
1272
|
-
}, async ({ file, task, title }) => {
|
|
1273
|
-
const filePath = file || defaultFile;
|
|
1274
|
-
// V2: update task file directly
|
|
1275
|
-
if ((0, v2_detect_1.isV2)(filePath)) {
|
|
1276
|
-
const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
|
|
1277
|
-
const found = (0, v2_detect_1.findV2Task)(dirs, task);
|
|
1278
|
-
if (!found) {
|
|
1279
|
-
return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
|
|
1280
|
-
}
|
|
1281
|
-
const t = found.doc.task;
|
|
1282
|
-
if (!t.subtasks)
|
|
1283
|
-
t.subtasks = [];
|
|
1284
|
-
const nextId = t.subtasks.length > 0
|
|
1285
|
-
? `${task}-${Math.max(...t.subtasks.map(s => parseInt(s.id.split('-').pop() || '0', 10))) + 1}`
|
|
1286
|
-
: `${task}-1`;
|
|
1287
|
-
const newSubtask = { id: nextId, title, completed: false };
|
|
1288
|
-
t.subtasks.push(newSubtask);
|
|
1289
|
-
t.updatedAt = new Date().toISOString();
|
|
1290
|
-
(0, core_3.writeTaskFile)(found.filePath, t, found.doc.body);
|
|
1291
|
-
return { content: [{ type: 'text', text: `Subtask added: ${newSubtask.id} - ${newSubtask.title}` }] };
|
|
1292
|
-
}
|
|
1293
|
-
// V1: use board
|
|
1294
|
-
const result = readBoard(filePath);
|
|
1295
|
-
if ('error' in result) {
|
|
1296
|
-
return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
|
|
1297
|
-
}
|
|
1298
|
-
let { board } = result;
|
|
1299
|
-
const addResult = (0, core_1.addSubtask)(board, task, title);
|
|
1300
|
-
if (!addResult.success) {
|
|
1301
|
-
return { content: [{ type: 'text', text: `Error: ${addResult.error}` }], isError: true };
|
|
1302
|
-
}
|
|
1303
|
-
writeBoard(filePath, addResult.board);
|
|
1304
|
-
// Get the new subtask ID
|
|
1305
|
-
const updatedTask = (0, core_1.findTaskById)(addResult.board, task).task;
|
|
1306
|
-
const newSubtask = updatedTask.subtasks.slice(-1)[0];
|
|
1307
|
-
return {
|
|
1308
|
-
content: [{ type: 'text', text: `Subtask added: ${newSubtask.id} - ${newSubtask.title}` }]
|
|
1309
|
-
};
|
|
1310
|
-
});
|
|
1311
|
-
// Delete subtask tool
|
|
1312
|
-
server.registerTool('delete_subtask', {
|
|
1313
|
-
title: 'Delete Subtask',
|
|
1314
|
-
description: 'Delete a subtask from a task',
|
|
1315
|
-
inputSchema: {
|
|
1316
|
-
file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
|
|
1317
|
-
task: zod_1.z.string().describe('Parent task ID'),
|
|
1318
|
-
subtask: zod_1.z.string().describe('Subtask ID to delete')
|
|
1319
|
-
}
|
|
1320
|
-
}, async ({ file, task, subtask }) => {
|
|
1321
|
-
const filePath = file || defaultFile;
|
|
1322
|
-
// V2: update task file directly
|
|
1323
|
-
if ((0, v2_detect_1.isV2)(filePath)) {
|
|
1324
|
-
const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
|
|
1325
|
-
const found = (0, v2_detect_1.findV2Task)(dirs, task);
|
|
1326
|
-
if (!found) {
|
|
1327
|
-
return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
|
|
1328
|
-
}
|
|
1329
|
-
const t = found.doc.task;
|
|
1330
|
-
if (!t.subtasks || !t.subtasks.some(s => s.id === subtask)) {
|
|
1331
|
-
return { content: [{ type: 'text', text: `Error: Subtask not found: ${subtask}` }], isError: true };
|
|
1332
|
-
}
|
|
1333
|
-
t.subtasks = t.subtasks.filter(s => s.id !== subtask);
|
|
1334
|
-
t.updatedAt = new Date().toISOString();
|
|
1335
|
-
(0, core_3.writeTaskFile)(found.filePath, t, found.doc.body);
|
|
1336
|
-
return { content: [{ type: 'text', text: `Subtask ${subtask} deleted successfully` }] };
|
|
1337
|
-
}
|
|
1338
|
-
// V1: use board
|
|
1339
|
-
const result = readBoard(filePath);
|
|
1340
|
-
if ('error' in result) {
|
|
1341
|
-
return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
|
|
1342
|
-
}
|
|
1343
|
-
let { board } = result;
|
|
1344
|
-
const deleteResult = (0, core_1.deleteSubtask)(board, task, subtask);
|
|
1345
|
-
if (!deleteResult.success) {
|
|
1346
|
-
return { content: [{ type: 'text', text: `Error: ${deleteResult.error}` }], isError: true };
|
|
1347
|
-
}
|
|
1348
|
-
writeBoard(filePath, deleteResult.board);
|
|
1349
|
-
return {
|
|
1350
|
-
content: [{ type: 'text', text: `Subtask ${subtask} deleted successfully` }]
|
|
1351
|
-
};
|
|
1352
|
-
});
|
|
1353
|
-
// Toggle subtask tool
|
|
1354
|
-
server.registerTool('toggle_subtask', {
|
|
1355
|
-
title: 'Toggle Subtask',
|
|
1356
|
-
description: 'Toggle a subtask completion status',
|
|
1357
|
-
inputSchema: {
|
|
1358
|
-
file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
|
|
1359
|
-
task: zod_1.z.string().describe('Parent task ID'),
|
|
1360
|
-
subtask: zod_1.z.string().describe('Subtask ID to toggle')
|
|
1361
|
-
}
|
|
1362
|
-
}, async ({ file, task, subtask }) => {
|
|
1363
|
-
const filePath = file || defaultFile;
|
|
1364
|
-
// V2: update task file directly
|
|
1365
|
-
if ((0, v2_detect_1.isV2)(filePath)) {
|
|
1366
|
-
const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
|
|
1367
|
-
const found = (0, v2_detect_1.findV2Task)(dirs, task);
|
|
1368
|
-
if (!found) {
|
|
1369
|
-
return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
|
|
1370
|
-
}
|
|
1371
|
-
const t = found.doc.task;
|
|
1372
|
-
const st = t.subtasks?.find(s => s.id === subtask);
|
|
1373
|
-
if (!st) {
|
|
1374
|
-
return { content: [{ type: 'text', text: `Error: Subtask not found: ${subtask}` }], isError: true };
|
|
1375
|
-
}
|
|
1376
|
-
const wasCompleted = st.completed;
|
|
1377
|
-
st.completed = !st.completed;
|
|
1378
|
-
t.updatedAt = new Date().toISOString();
|
|
1379
|
-
(0, core_3.writeTaskFile)(found.filePath, t, found.doc.body);
|
|
1380
|
-
const newStatus = wasCompleted ? 'incomplete' : 'completed';
|
|
1381
|
-
return { content: [{ type: 'text', text: `Subtask ${subtask} marked as ${newStatus}` }] };
|
|
1382
|
-
}
|
|
1383
|
-
// V1: use board
|
|
1384
|
-
const result = readBoard(filePath);
|
|
1385
|
-
if ('error' in result) {
|
|
1386
|
-
return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
|
|
1387
|
-
}
|
|
1388
|
-
let { board } = result;
|
|
1389
|
-
// Get current status
|
|
1390
|
-
const taskInfo = (0, core_1.findTaskById)(board, task);
|
|
1391
|
-
const currentSubtask = taskInfo?.task.subtasks?.find(st => st.id === subtask);
|
|
1392
|
-
const wasCompleted = currentSubtask?.completed || false;
|
|
1393
|
-
const toggleResult = (0, core_1.toggleSubtask)(board, task, subtask);
|
|
1394
|
-
if (!toggleResult.success) {
|
|
1395
|
-
return { content: [{ type: 'text', text: `Error: ${toggleResult.error}` }], isError: true };
|
|
1396
|
-
}
|
|
1397
|
-
writeBoard(filePath, toggleResult.board);
|
|
1398
|
-
const newStatus = wasCompleted ? 'incomplete' : 'completed';
|
|
1399
|
-
return {
|
|
1400
|
-
content: [{ type: 'text', text: `Subtask ${subtask} marked as ${newStatus}` }]
|
|
1401
|
-
};
|
|
1402
|
-
});
|
|
1403
|
-
// Update subtask tool
|
|
1404
|
-
server.registerTool('update_subtask', {
|
|
1405
|
-
title: 'Update Subtask',
|
|
1406
|
-
description: 'Update a subtask title',
|
|
1407
|
-
inputSchema: {
|
|
1408
|
-
file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
|
|
1409
|
-
task: zod_1.z.string().describe('Parent task ID'),
|
|
1410
|
-
subtask: zod_1.z.string().describe('Subtask ID to update'),
|
|
1411
|
-
title: zod_1.z.string().describe('New subtask title')
|
|
1412
|
-
}
|
|
1413
|
-
}, async ({ file, task, subtask, title }) => {
|
|
1414
|
-
const filePath = file || defaultFile;
|
|
1415
|
-
// V2: update task file directly
|
|
1416
|
-
if ((0, v2_detect_1.isV2)(filePath)) {
|
|
1417
|
-
const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
|
|
1418
|
-
const found = (0, v2_detect_1.findV2Task)(dirs, task);
|
|
1419
|
-
if (!found) {
|
|
1420
|
-
return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
|
|
1421
|
-
}
|
|
1422
|
-
const t = found.doc.task;
|
|
1423
|
-
const st = t.subtasks?.find(s => s.id === subtask);
|
|
1424
|
-
if (!st) {
|
|
1425
|
-
return { content: [{ type: 'text', text: `Error: Subtask not found: ${subtask}` }], isError: true };
|
|
1426
|
-
}
|
|
1427
|
-
st.title = title;
|
|
1428
|
-
t.updatedAt = new Date().toISOString();
|
|
1429
|
-
(0, core_3.writeTaskFile)(found.filePath, t, found.doc.body);
|
|
1430
|
-
return { content: [{ type: 'text', text: `Subtask ${subtask} updated to "${title}"` }] };
|
|
1431
|
-
}
|
|
1432
|
-
// V1: use board
|
|
1433
|
-
const result = readBoard(filePath);
|
|
1434
|
-
if ('error' in result) {
|
|
1435
|
-
return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
|
|
1436
|
-
}
|
|
1437
|
-
let { board } = result;
|
|
1438
|
-
const updateResult = (0, core_1.updateSubtask)(board, task, subtask, title);
|
|
1439
|
-
if (!updateResult.success) {
|
|
1440
|
-
return { content: [{ type: 'text', text: `Error: ${updateResult.error}` }], isError: true };
|
|
1441
|
-
}
|
|
1442
|
-
writeBoard(filePath, updateResult.board);
|
|
1443
|
-
return {
|
|
1444
|
-
content: [{ type: 'text', text: `Subtask ${subtask} updated to "${title}"` }]
|
|
1445
|
-
};
|
|
1446
|
-
});
|
|
1447
|
-
// Bulk set subtasks completed tool
|
|
1448
|
-
server.registerTool('bulk_set_subtasks', {
|
|
1449
|
-
title: 'Bulk Set Subtasks',
|
|
1450
|
-
description: 'Set multiple subtasks to completed or incomplete in a single atomic operation',
|
|
1451
|
-
inputSchema: {
|
|
1452
|
-
file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
|
|
1453
|
-
task: zod_1.z.string().describe('Parent task ID'),
|
|
1454
|
-
subtasks: zod_1.z.array(zod_1.z.string()).describe('Array of subtask IDs to update'),
|
|
1455
|
-
completed: zod_1.z.boolean().describe('Whether to mark as completed (true) or incomplete (false)')
|
|
1456
|
-
}
|
|
1457
|
-
}, async ({ file, task, subtasks, completed }) => {
|
|
1458
|
-
const filePath = file || defaultFile;
|
|
1459
|
-
// V2: update task file directly
|
|
1460
|
-
if ((0, v2_detect_1.isV2)(filePath)) {
|
|
1461
|
-
const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
|
|
1462
|
-
const found = (0, v2_detect_1.findV2Task)(dirs, task);
|
|
1463
|
-
if (!found) {
|
|
1464
|
-
return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
|
|
1465
|
-
}
|
|
1466
|
-
const t = found.doc.task;
|
|
1467
|
-
if (!t.subtasks) {
|
|
1468
|
-
return { content: [{ type: 'text', text: `Error: Task has no subtasks` }], isError: true };
|
|
1469
|
-
}
|
|
1470
|
-
const subtaskSet = new Set(subtasks);
|
|
1471
|
-
for (const st of t.subtasks) {
|
|
1472
|
-
if (subtaskSet.has(st.id)) {
|
|
1473
|
-
st.completed = completed;
|
|
1474
|
-
}
|
|
1475
|
-
}
|
|
1476
|
-
t.updatedAt = new Date().toISOString();
|
|
1477
|
-
(0, core_3.writeTaskFile)(found.filePath, t, found.doc.body);
|
|
1478
|
-
const status = completed ? 'completed' : 'incomplete';
|
|
1479
|
-
return { content: [{ type: 'text', text: `${subtasks.length} subtasks marked as ${status}` }] };
|
|
1480
|
-
}
|
|
1481
|
-
// V1: use board
|
|
1482
|
-
const result = readBoard(filePath);
|
|
1483
|
-
if ('error' in result) {
|
|
1484
|
-
return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
|
|
1485
|
-
}
|
|
1486
|
-
let { board } = result;
|
|
1487
|
-
const bulkResult = (0, core_1.setSubtasksCompleted)(board, task, subtasks, completed);
|
|
1488
|
-
if (!bulkResult.success) {
|
|
1489
|
-
return { content: [{ type: 'text', text: `Error: ${bulkResult.error}` }], isError: true };
|
|
1490
|
-
}
|
|
1491
|
-
writeBoard(filePath, bulkResult.board);
|
|
1492
|
-
const status = completed ? 'completed' : 'incomplete';
|
|
1493
|
-
return {
|
|
1494
|
-
content: [{ type: 'text', text: `${subtasks.length} subtasks marked as ${status}` }]
|
|
1495
|
-
};
|
|
1496
|
-
});
|
|
1497
|
-
// Complete all subtasks tool
|
|
1498
|
-
server.registerTool('complete_all_subtasks', {
|
|
1499
|
-
title: 'Complete All Subtasks',
|
|
1500
|
-
description: 'Mark all subtasks in a task as completed or incomplete',
|
|
1501
|
-
inputSchema: {
|
|
1502
|
-
file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
|
|
1503
|
-
task: zod_1.z.string().describe('Parent task ID'),
|
|
1504
|
-
completed: zod_1.z.boolean().optional().default(true).describe('Whether to mark as completed (default: true) or incomplete (false)')
|
|
1505
|
-
}
|
|
1506
|
-
}, async ({ file, task, completed }) => {
|
|
1507
|
-
const filePath = file || defaultFile;
|
|
1508
|
-
const markCompleted = completed ?? true;
|
|
1509
|
-
// V2: update task file directly
|
|
1510
|
-
if ((0, v2_detect_1.isV2)(filePath)) {
|
|
1511
|
-
const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
|
|
1512
|
-
const found = (0, v2_detect_1.findV2Task)(dirs, task);
|
|
1513
|
-
if (!found) {
|
|
1514
|
-
return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
|
|
1515
|
-
}
|
|
1516
|
-
const t = found.doc.task;
|
|
1517
|
-
const count = t.subtasks?.length || 0;
|
|
1518
|
-
if (t.subtasks) {
|
|
1519
|
-
for (const st of t.subtasks) {
|
|
1520
|
-
st.completed = markCompleted;
|
|
1521
|
-
}
|
|
1522
|
-
}
|
|
1523
|
-
t.updatedAt = new Date().toISOString();
|
|
1524
|
-
(0, core_3.writeTaskFile)(found.filePath, t, found.doc.body);
|
|
1525
|
-
const status = markCompleted ? 'completed' : 'incomplete';
|
|
1526
|
-
return { content: [{ type: 'text', text: `All ${count} subtasks in ${task} marked as ${status}` }] };
|
|
1527
|
-
}
|
|
1528
|
-
// V1: use board
|
|
1529
|
-
const result = readBoard(filePath);
|
|
1530
|
-
if ('error' in result) {
|
|
1531
|
-
return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
|
|
1532
|
-
}
|
|
1533
|
-
let { board } = result;
|
|
1534
|
-
const bulkResult = (0, core_1.setAllSubtasksCompleted)(board, task, markCompleted);
|
|
1535
|
-
if (!bulkResult.success) {
|
|
1536
|
-
return { content: [{ type: 'text', text: `Error: ${bulkResult.error}` }], isError: true };
|
|
1537
|
-
}
|
|
1538
|
-
writeBoard(filePath, bulkResult.board);
|
|
1539
|
-
// Count subtasks for the message
|
|
1540
|
-
const taskInfo = (0, core_1.findTaskById)(bulkResult.board, task);
|
|
1541
|
-
const count = taskInfo?.task.subtasks?.length || 0;
|
|
1542
|
-
const status = markCompleted ? 'completed' : 'incomplete';
|
|
1543
|
-
return {
|
|
1544
|
-
content: [{ type: 'text', text: `All ${count} subtasks in ${task} marked as ${status}` }]
|
|
1545
|
-
};
|
|
1546
|
-
});
|
|
1547
|
-
// ==========================================================================
|
|
1548
|
-
// BULK OPERATIONS
|
|
1549
|
-
// ==========================================================================
|
|
1550
|
-
// Bulk move tasks tool
|
|
1551
|
-
server.registerTool('bulk_move_tasks', {
|
|
1552
|
-
title: 'Bulk Move Tasks',
|
|
1553
|
-
description: 'Move multiple tasks to a target column in a single operation',
|
|
1554
|
-
inputSchema: {
|
|
1555
|
-
file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
|
|
1556
|
-
tasks: zod_1.z.array(zod_1.z.string()).describe('Array of task IDs to move'),
|
|
1557
|
-
column: zod_1.z.string().describe('Target column ID or name')
|
|
1558
|
-
}
|
|
1559
|
-
}, async ({ file, tasks, column }) => {
|
|
1560
|
-
const filePath = file || defaultFile;
|
|
1561
|
-
// V2: update each task file
|
|
1562
|
-
if ((0, v2_detect_1.isV2)(filePath)) {
|
|
1563
|
-
const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
|
|
1564
|
-
const board = (0, v2_detect_1.readV2BoardConfig)(filePath);
|
|
1565
|
-
let targetColumn = board.columns.find(c => c.id === column);
|
|
1566
|
-
if (!targetColumn)
|
|
1567
|
-
targetColumn = board.columns.find(c => c.title.toLowerCase() === column.toLowerCase());
|
|
1568
|
-
if (!targetColumn) {
|
|
1569
|
-
return { content: [{ type: 'text', text: `Error: Column not found: ${column}` }], isError: true };
|
|
1570
|
-
}
|
|
1571
|
-
const results = [];
|
|
1572
|
-
let successCount = 0;
|
|
1573
|
-
let failureCount = 0;
|
|
1574
|
-
for (const taskId of tasks) {
|
|
1575
|
-
const taskPath = path.join(dirs.boardDir, (0, core_3.taskFileName)(taskId));
|
|
1576
|
-
const doc = (0, core_3.readTaskFile)(taskPath);
|
|
1577
|
-
if (!doc) {
|
|
1578
|
-
results.push({ taskId, success: false, error: 'Task not found' });
|
|
1579
|
-
failureCount++;
|
|
1580
|
-
continue;
|
|
1581
|
-
}
|
|
1582
|
-
doc.task.column = targetColumn.id;
|
|
1583
|
-
doc.task.updatedAt = new Date().toISOString();
|
|
1584
|
-
(0, core_3.writeTaskFile)(taskPath, doc.task, doc.body);
|
|
1585
|
-
results.push({ taskId, success: true });
|
|
1586
|
-
successCount++;
|
|
1587
|
-
}
|
|
1588
|
-
const output = { success: failureCount === 0, successCount, failureCount, results };
|
|
1589
|
-
return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }], isError: failureCount > 0 && successCount === 0 };
|
|
1590
|
-
}
|
|
1591
|
-
// V1: use board
|
|
1592
|
-
const result = readBoard(filePath);
|
|
1593
|
-
if ('error' in result) {
|
|
1594
|
-
return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
|
|
1595
|
-
}
|
|
1596
|
-
let { board } = result;
|
|
1597
|
-
// Find target column
|
|
1598
|
-
let targetColumn = (0, core_1.findColumnById)(board, column);
|
|
1599
|
-
if (!targetColumn) {
|
|
1600
|
-
targetColumn = (0, core_1.findColumnByName)(board, column);
|
|
1601
|
-
}
|
|
1602
|
-
if (!targetColumn) {
|
|
1603
|
-
return { content: [{ type: 'text', text: `Error: Column not found: ${column}` }], isError: true };
|
|
1604
|
-
}
|
|
1605
|
-
// Check for incomplete subtasks before move (for warning)
|
|
1606
|
-
const tasksWithIncomplete = [];
|
|
1607
|
-
for (const taskId of tasks) {
|
|
1608
|
-
const taskInfo = (0, core_1.findTaskById)(board, taskId);
|
|
1609
|
-
if (taskInfo) {
|
|
1610
|
-
const warning = (0, errorHandler_1.mcpCheckIncompleteSubtasks)(taskInfo.task, targetColumn);
|
|
1611
|
-
if (warning?.incompleteSubtasks) {
|
|
1612
|
-
tasksWithIncomplete.push({
|
|
1613
|
-
id: taskId,
|
|
1614
|
-
incomplete: warning.incompleteSubtasks.incomplete.length,
|
|
1615
|
-
total: warning.incompleteSubtasks.total
|
|
1616
|
-
});
|
|
1617
|
-
}
|
|
1618
|
-
}
|
|
1619
|
-
}
|
|
1620
|
-
const bulkResult = (0, core_1.moveTasks)(board, tasks, targetColumn.id);
|
|
1621
|
-
if (bulkResult.board) {
|
|
1622
|
-
writeBoard(filePath, bulkResult.board);
|
|
1623
|
-
}
|
|
1624
|
-
const output = {
|
|
1625
|
-
success: bulkResult.success,
|
|
1626
|
-
successCount: bulkResult.successCount,
|
|
1627
|
-
failureCount: bulkResult.failureCount,
|
|
1628
|
-
results: bulkResult.results
|
|
1629
|
-
};
|
|
1630
|
-
// Add warning about incomplete subtasks if any
|
|
1631
|
-
if (tasksWithIncomplete.length > 0) {
|
|
1632
|
-
output.warning = `${tasksWithIncomplete.length} task(s) moved to "${targetColumn.title}" have incomplete subtasks`;
|
|
1633
|
-
output.tasksWithIncompleteSubtasks = tasksWithIncomplete;
|
|
1634
|
-
}
|
|
1635
|
-
return {
|
|
1636
|
-
content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
|
|
1637
|
-
isError: !bulkResult.success
|
|
1638
|
-
};
|
|
1639
|
-
});
|
|
1640
|
-
// Bulk patch tasks tool
|
|
1641
|
-
server.registerTool('bulk_patch_tasks', {
|
|
1642
|
-
title: 'Bulk Patch Tasks',
|
|
1643
|
-
description: 'Apply the same patch to multiple tasks in a single operation',
|
|
1644
|
-
inputSchema: {
|
|
1645
|
-
file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
|
|
1646
|
-
tasks: zod_1.z.array(zod_1.z.string()).describe('Array of task IDs to patch'),
|
|
1647
|
-
priority: zod_1.z.enum(['low', 'medium', 'high', 'critical']).nullable().optional().describe('New priority (null to remove)'),
|
|
1648
|
-
tags: zod_1.z.array(zod_1.z.string()).nullable().optional().describe('New tags (null to remove)'),
|
|
1649
|
-
assignee: zod_1.z.string().nullable().optional().describe('New assignee (null to remove)')
|
|
1650
|
-
}
|
|
1651
|
-
}, async ({ file, tasks, priority, tags, assignee }) => {
|
|
1652
|
-
const filePath = file || defaultFile;
|
|
1653
|
-
const isNull = (v) => v === null || v === 'null';
|
|
1654
|
-
// V2: update each task file
|
|
1655
|
-
if ((0, v2_detect_1.isV2)(filePath)) {
|
|
1656
|
-
const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
|
|
1657
|
-
const results = [];
|
|
1658
|
-
let successCount = 0;
|
|
1659
|
-
let failureCount = 0;
|
|
1660
|
-
for (const taskId of tasks) {
|
|
1661
|
-
const taskPath = path.join(dirs.boardDir, (0, core_3.taskFileName)(taskId));
|
|
1662
|
-
const doc = (0, core_3.readTaskFile)(taskPath);
|
|
1663
|
-
if (!doc) {
|
|
1664
|
-
results.push({ taskId, success: false, error: 'Task not found' });
|
|
1665
|
-
failureCount++;
|
|
1666
|
-
continue;
|
|
1667
|
-
}
|
|
1668
|
-
const t = doc.task;
|
|
1669
|
-
if (priority !== undefined) {
|
|
1670
|
-
if (isNull(priority))
|
|
1671
|
-
delete t.priority;
|
|
1672
|
-
else
|
|
1673
|
-
t.priority = priority;
|
|
1674
|
-
}
|
|
1675
|
-
if (tags !== undefined) {
|
|
1676
|
-
if (isNull(tags))
|
|
1677
|
-
delete t.tags;
|
|
1678
|
-
else
|
|
1679
|
-
t.tags = tags;
|
|
1680
|
-
}
|
|
1681
|
-
if (assignee !== undefined) {
|
|
1682
|
-
if (isNull(assignee))
|
|
1683
|
-
delete t.assignee;
|
|
1684
|
-
else
|
|
1685
|
-
t.assignee = assignee;
|
|
1686
|
-
}
|
|
1687
|
-
t.updatedAt = new Date().toISOString();
|
|
1688
|
-
(0, core_3.writeTaskFile)(taskPath, t, doc.body);
|
|
1689
|
-
results.push({ taskId, success: true });
|
|
1690
|
-
successCount++;
|
|
1691
|
-
}
|
|
1692
|
-
const output = { success: failureCount === 0, successCount, failureCount, results };
|
|
1693
|
-
return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }], isError: failureCount > 0 && successCount === 0 };
|
|
1694
|
-
}
|
|
1695
|
-
// V1: use board
|
|
1696
|
-
const result = readBoard(filePath);
|
|
1697
|
-
if ('error' in result) {
|
|
1698
|
-
return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
|
|
1699
|
-
}
|
|
1700
|
-
let { board } = result;
|
|
1701
|
-
const patch = {};
|
|
1702
|
-
if (priority !== undefined)
|
|
1703
|
-
patch.priority = isNull(priority) ? undefined : priority;
|
|
1704
|
-
if (tags !== undefined)
|
|
1705
|
-
patch.tags = isNull(tags) ? undefined : tags;
|
|
1706
|
-
if (assignee !== undefined)
|
|
1707
|
-
patch.assignee = isNull(assignee) ? undefined : assignee;
|
|
1708
|
-
const bulkResult = (0, core_1.patchTasks)(board, tasks, patch);
|
|
1709
|
-
if (bulkResult.board) {
|
|
1710
|
-
writeBoard(filePath, bulkResult.board);
|
|
1711
|
-
}
|
|
1712
|
-
const output = {
|
|
1713
|
-
success: bulkResult.success,
|
|
1714
|
-
successCount: bulkResult.successCount,
|
|
1715
|
-
failureCount: bulkResult.failureCount,
|
|
1716
|
-
results: bulkResult.results
|
|
1717
|
-
};
|
|
1718
|
-
return {
|
|
1719
|
-
content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
|
|
1720
|
-
isError: !bulkResult.success
|
|
1721
|
-
};
|
|
1722
|
-
});
|
|
1723
|
-
// Bulk delete tasks tool
|
|
1724
|
-
server.registerTool('bulk_delete_tasks', {
|
|
1725
|
-
title: 'Bulk Delete Tasks',
|
|
1726
|
-
description: 'Permanently delete multiple tasks in a single operation',
|
|
1727
|
-
inputSchema: {
|
|
1728
|
-
file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
|
|
1729
|
-
tasks: zod_1.z.array(zod_1.z.string()).describe('Array of task IDs to delete')
|
|
1730
|
-
}
|
|
1731
|
-
}, async ({ file, tasks }) => {
|
|
1732
|
-
const filePath = file || defaultFile;
|
|
1733
|
-
// V2: delete task files
|
|
1734
|
-
if ((0, v2_detect_1.isV2)(filePath)) {
|
|
1735
|
-
const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
|
|
1736
|
-
const results = [];
|
|
1737
|
-
let successCount = 0;
|
|
1738
|
-
let failureCount = 0;
|
|
1739
|
-
for (const taskId of tasks) {
|
|
1740
|
-
const found = (0, v2_detect_1.findV2Task)(dirs, taskId, true);
|
|
1741
|
-
if (!found) {
|
|
1742
|
-
results.push({ taskId, success: false, error: 'Task not found' });
|
|
1743
|
-
failureCount++;
|
|
1744
|
-
continue;
|
|
1745
|
-
}
|
|
1746
|
-
try {
|
|
1747
|
-
fs.unlinkSync(found.filePath);
|
|
1748
|
-
results.push({ taskId, success: true });
|
|
1749
|
-
successCount++;
|
|
1750
|
-
}
|
|
1751
|
-
catch (e) {
|
|
1752
|
-
results.push({ taskId, success: false, error: e.message });
|
|
1753
|
-
failureCount++;
|
|
1754
|
-
}
|
|
1755
|
-
}
|
|
1756
|
-
const output = { success: failureCount === 0, successCount, failureCount, results };
|
|
1757
|
-
return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }], isError: failureCount > 0 && successCount === 0 };
|
|
1758
|
-
}
|
|
1759
|
-
// V1: use board
|
|
1760
|
-
const result = readBoard(filePath);
|
|
1761
|
-
if ('error' in result) {
|
|
1762
|
-
return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
|
|
1763
|
-
}
|
|
1764
|
-
let { board } = result;
|
|
1765
|
-
const bulkResult = (0, core_1.deleteTasks)(board, tasks);
|
|
1766
|
-
if (bulkResult.board) {
|
|
1767
|
-
writeBoard(filePath, bulkResult.board);
|
|
1768
|
-
}
|
|
1769
|
-
const output = {
|
|
1770
|
-
success: bulkResult.success,
|
|
1771
|
-
successCount: bulkResult.successCount,
|
|
1772
|
-
failureCount: bulkResult.failureCount,
|
|
1773
|
-
results: bulkResult.results
|
|
1774
|
-
};
|
|
1775
|
-
return {
|
|
1776
|
-
content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
|
|
1777
|
-
isError: !bulkResult.success
|
|
1778
|
-
};
|
|
1779
|
-
});
|
|
1780
|
-
// Bulk archive tasks tool
|
|
1781
|
-
server.registerTool('bulk_archive_tasks', {
|
|
1782
|
-
title: 'Bulk Archive Tasks',
|
|
1783
|
-
description: 'Archive multiple tasks to the separate archive file',
|
|
1784
|
-
inputSchema: {
|
|
1785
|
-
file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
|
|
1786
|
-
tasks: zod_1.z.array(zod_1.z.string()).describe('Array of task IDs to archive')
|
|
1787
|
-
}
|
|
1788
|
-
}, async ({ file, tasks }) => {
|
|
1789
|
-
const filePath = file || defaultFile;
|
|
1790
|
-
// V2: move task files from board/ to logs/
|
|
1791
|
-
if ((0, v2_detect_1.isV2)(filePath)) {
|
|
1792
|
-
const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
|
|
1793
|
-
const results = [];
|
|
1794
|
-
let successCount = 0;
|
|
1795
|
-
let failureCount = 0;
|
|
1796
|
-
for (const taskId of tasks) {
|
|
1797
|
-
const taskPath = path.join(dirs.boardDir, (0, core_3.taskFileName)(taskId));
|
|
1798
|
-
const doc = (0, core_3.readTaskFile)(taskPath);
|
|
1799
|
-
if (!doc) {
|
|
1800
|
-
results.push({ taskId, success: false, error: 'Task not found' });
|
|
1801
|
-
failureCount++;
|
|
1802
|
-
continue;
|
|
1803
|
-
}
|
|
1804
|
-
try {
|
|
1805
|
-
const logPath = path.join(dirs.logsDir, (0, core_3.taskFileName)(taskId));
|
|
1806
|
-
doc.task.completedAt = doc.task.completedAt || new Date().toISOString();
|
|
1807
|
-
delete doc.task.column;
|
|
1808
|
-
delete doc.task.position;
|
|
1809
|
-
(0, core_3.writeTaskFile)(logPath, doc.task, doc.body);
|
|
1810
|
-
fs.unlinkSync(taskPath);
|
|
1811
|
-
results.push({ taskId, success: true });
|
|
1812
|
-
successCount++;
|
|
1813
|
-
}
|
|
1814
|
-
catch (e) {
|
|
1815
|
-
results.push({ taskId, success: false, error: e.message });
|
|
1816
|
-
failureCount++;
|
|
1817
|
-
}
|
|
1818
|
-
}
|
|
1819
|
-
const output = { success: failureCount === 0, successCount, failureCount, results };
|
|
1820
|
-
return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }], isError: failureCount > 0 && successCount === 0 };
|
|
1821
|
-
}
|
|
1822
|
-
// V1: use board
|
|
1823
|
-
const results = [];
|
|
1824
|
-
let successCount = 0;
|
|
1825
|
-
let failureCount = 0;
|
|
1826
|
-
// Archive each task individually (need to re-read board after each archive)
|
|
1827
|
-
for (const taskId of tasks) {
|
|
1828
|
-
const boardResult = readBoard(filePath);
|
|
1829
|
-
if ('error' in boardResult) {
|
|
1830
|
-
results.push({ taskId, success: false, error: boardResult.error });
|
|
1831
|
-
failureCount++;
|
|
1832
|
-
continue;
|
|
1833
|
-
}
|
|
1834
|
-
const { board } = boardResult;
|
|
1835
|
-
const taskInfo = (0, core_1.findTaskById)(board, taskId);
|
|
1836
|
-
if (!taskInfo) {
|
|
1837
|
-
results.push({ taskId, success: false, error: 'Task not found' });
|
|
1838
|
-
failureCount++;
|
|
1839
|
-
continue;
|
|
1840
|
-
}
|
|
1841
|
-
const archiveResult = (0, archive_1.archiveTaskToFile)(filePath, board, taskInfo.column.id, taskId);
|
|
1842
|
-
if (archiveResult.success) {
|
|
1843
|
-
results.push({ taskId, success: true });
|
|
1844
|
-
successCount++;
|
|
1845
|
-
}
|
|
1846
|
-
else {
|
|
1847
|
-
results.push({ taskId, success: false, error: archiveResult.error });
|
|
1848
|
-
failureCount++;
|
|
1849
|
-
}
|
|
1850
|
-
}
|
|
1851
|
-
const output = {
|
|
1852
|
-
success: failureCount === 0,
|
|
1853
|
-
successCount,
|
|
1854
|
-
failureCount,
|
|
1855
|
-
results,
|
|
1856
|
-
archiveFile: path.basename((0, archive_1.getArchivePath)(filePath))
|
|
1857
|
-
};
|
|
1858
|
-
return {
|
|
1859
|
-
content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
|
|
1860
|
-
isError: failureCount > 0 && successCount === 0
|
|
1861
|
-
};
|
|
1862
|
-
});
|
|
1863
|
-
// ==========================================================================
|
|
1864
|
-
// CONTRACTS
|
|
1865
|
-
// ==========================================================================
|
|
1866
|
-
server.registerTool('contract_pickup', {
|
|
1867
|
-
title: 'Contract Pickup',
|
|
1868
|
-
description: 'Claim a task contract (sets status to in_progress) and return agent context as markdown',
|
|
1869
|
-
inputSchema: {
|
|
1870
|
-
file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
|
|
1871
|
-
task: zod_1.z.string().describe('Task ID to pick up'),
|
|
1872
|
-
}
|
|
1873
|
-
}, async ({ file, task }) => {
|
|
1874
|
-
const filePath = file || defaultFile;
|
|
1875
|
-
const result = (0, contractRunner_1.pickupContract)({ filePath, taskId: task });
|
|
1876
|
-
if ('error' in result) {
|
|
1877
|
-
return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
|
|
1878
|
-
}
|
|
1879
|
-
return { content: [{ type: 'text', text: result.markdown }] };
|
|
1880
|
-
});
|
|
1881
|
-
server.registerTool('contract_deliver', {
|
|
1882
|
-
title: 'Contract Deliver',
|
|
1883
|
-
description: 'Mark a task contract as delivered (sets status to delivered)',
|
|
1884
|
-
inputSchema: {
|
|
1885
|
-
file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
|
|
1886
|
-
task: zod_1.z.string().describe('Task ID to deliver'),
|
|
1887
|
-
}
|
|
1888
|
-
}, async ({ file, task }) => {
|
|
1889
|
-
const filePath = file || defaultFile;
|
|
1890
|
-
const result = (0, contractRunner_1.deliverContract)({ filePath, taskId: task });
|
|
1891
|
-
if ('error' in result) {
|
|
1892
|
-
return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
|
|
1893
|
-
}
|
|
1894
|
-
return {
|
|
1895
|
-
content: [{
|
|
1896
|
-
type: 'text',
|
|
1897
|
-
text: `Contract delivered: ${task}\nReminder: prepare summary and filesChanged for the upcoming complete_task call.`
|
|
1898
|
-
}]
|
|
1899
|
-
};
|
|
1900
|
-
});
|
|
1901
|
-
server.registerTool('contract_validate', {
|
|
1902
|
-
title: 'Contract Validate',
|
|
1903
|
-
description: 'Validate contract deliverables + commands; sets status to done/failed',
|
|
1904
|
-
inputSchema: {
|
|
1905
|
-
file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
|
|
1906
|
-
task: zod_1.z.string().describe('Task ID to validate'),
|
|
1907
|
-
}
|
|
1908
|
-
}, async ({ file, task }) => {
|
|
1909
|
-
const filePath = file || defaultFile;
|
|
1910
|
-
const result = (0, contractRunner_1.validateContract)({ filePath, taskId: task });
|
|
1911
|
-
if ('error' in result) {
|
|
1912
|
-
return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
|
|
1913
|
-
}
|
|
1914
|
-
const output = {
|
|
1915
|
-
ok: result.ok,
|
|
1916
|
-
status: result.ok ? 'done' : 'failed',
|
|
1917
|
-
deliverables: result.deliverableChecks,
|
|
1918
|
-
commands: result.commandResults,
|
|
1919
|
-
warnings: result.warnings,
|
|
1920
|
-
};
|
|
1921
|
-
return {
|
|
1922
|
-
content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
|
|
1923
|
-
isError: !result.ok
|
|
1924
|
-
};
|
|
1925
|
-
});
|
|
1926
|
-
// ==========================================================================
|
|
1927
|
-
// TYPES
|
|
1928
|
-
// ==========================================================================
|
|
1929
|
-
server.registerTool('list_types', {
|
|
1930
|
-
title: 'List Types',
|
|
1931
|
-
description: 'List board strict mode and custom type configuration',
|
|
1932
|
-
inputSchema: {
|
|
1933
|
-
file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
|
|
1934
|
-
}
|
|
1935
|
-
}, async ({ file }) => {
|
|
1936
|
-
const filePath = file || defaultFile;
|
|
1937
|
-
if ((0, v2_detect_1.isV2)(filePath)) {
|
|
1938
|
-
try {
|
|
1939
|
-
const board = (0, v2_detect_1.readV2BoardConfig)(filePath);
|
|
1940
|
-
const boardConfig = board;
|
|
1941
|
-
const output = {
|
|
1942
|
-
strict: boardConfig.strict === true,
|
|
1943
|
-
types: sanitizeTypesConfig(boardConfig.types),
|
|
1944
|
-
};
|
|
1945
|
-
return {
|
|
1946
|
-
content: [{ type: 'text', text: JSON.stringify(output, null, 2) }]
|
|
1947
|
-
};
|
|
1948
|
-
}
|
|
1949
|
-
catch (e) {
|
|
1950
|
-
return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
|
|
1951
|
-
}
|
|
1952
|
-
}
|
|
1953
|
-
const result = readBoard(filePath);
|
|
1954
|
-
if ('error' in result) {
|
|
1955
|
-
return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
|
|
1956
|
-
}
|
|
1957
|
-
const boardConfig = result.board;
|
|
1958
|
-
const strict = boardConfig.strict === true;
|
|
1959
|
-
const types = sanitizeTypesConfig(boardConfig.types);
|
|
1960
|
-
return {
|
|
1961
|
-
content: [{
|
|
1962
|
-
type: 'text',
|
|
1963
|
-
text: JSON.stringify({ strict, types }, null, 2)
|
|
1964
|
-
}]
|
|
1965
|
-
};
|
|
1966
|
-
});
|
|
1967
|
-
// ==========================================================================
|
|
1968
|
-
// RULES
|
|
1969
|
-
// ==========================================================================
|
|
1970
|
-
// List rules tool
|
|
1971
|
-
server.registerTool('list_rules', {
|
|
1972
|
-
title: 'List Rules',
|
|
1973
|
-
description: 'List all project rules (always, never, prefer, context) from the brainfile',
|
|
1974
|
-
inputSchema: {
|
|
1975
|
-
file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
|
|
1976
|
-
category: zod_1.z.enum(['always', 'never', 'prefer', 'context']).optional().describe('Filter by rule category')
|
|
1977
|
-
}
|
|
1978
|
-
}, async ({ file, category }) => {
|
|
1979
|
-
const filePath = file || defaultFile;
|
|
1980
|
-
const result = readBoard(filePath);
|
|
1981
|
-
if ('error' in result) {
|
|
1982
|
-
return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
|
|
1983
|
-
}
|
|
1984
|
-
const { board } = result;
|
|
1985
|
-
const rules = board.rules || {};
|
|
1986
|
-
// Filter by category if specified
|
|
1987
|
-
let outputRules = rules;
|
|
1988
|
-
if (category) {
|
|
1989
|
-
outputRules = { [category]: rules[category] || [] };
|
|
1990
|
-
}
|
|
1991
|
-
// Count total rules
|
|
1992
|
-
const countRules = (r) => (r.always?.length || 0) +
|
|
1993
|
-
(r.never?.length || 0) +
|
|
1994
|
-
(r.prefer?.length || 0) +
|
|
1995
|
-
(r.context?.length || 0);
|
|
1996
|
-
const output = {
|
|
1997
|
-
rules: outputRules,
|
|
1998
|
-
totalCount: category
|
|
1999
|
-
? (outputRules[category]?.length || 0)
|
|
2000
|
-
: countRules(rules),
|
|
2001
|
-
};
|
|
2002
|
-
return {
|
|
2003
|
-
content: [{ type: 'text', text: JSON.stringify(output, null, 2) }]
|
|
2004
|
-
};
|
|
2005
|
-
});
|
|
2006
|
-
// Add rule tool
|
|
2007
|
-
server.registerTool('add_rule', {
|
|
2008
|
-
title: 'Add Rule',
|
|
2009
|
-
description: 'Add a new project rule to the brainfile',
|
|
2010
|
-
inputSchema: {
|
|
2011
|
-
file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
|
|
2012
|
-
category: zod_1.z.enum(['always', 'never', 'prefer', 'context']).describe('Rule category'),
|
|
2013
|
-
text: zod_1.z.string().describe('Rule text/description')
|
|
2014
|
-
}
|
|
2015
|
-
}, async ({ file, category, text }) => {
|
|
2016
|
-
const filePath = file || defaultFile;
|
|
2017
|
-
const result = readBoard(filePath);
|
|
2018
|
-
if ('error' in result) {
|
|
2019
|
-
return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
|
|
2020
|
-
}
|
|
2021
|
-
let { board } = result;
|
|
2022
|
-
const addResult = (0, core_1.addRule)(board, category, text);
|
|
2023
|
-
if (!addResult.success || !addResult.board) {
|
|
2024
|
-
return { content: [{ type: 'text', text: `Error: ${addResult.error}` }], isError: true };
|
|
2025
|
-
}
|
|
2026
|
-
writeBoard(filePath, addResult.board);
|
|
2027
|
-
// Find the newly added rule (last one in the category)
|
|
2028
|
-
const newRules = addResult.board.rules?.[category] || [];
|
|
2029
|
-
const newRule = newRules[newRules.length - 1];
|
|
2030
|
-
const output = {
|
|
2031
|
-
success: true,
|
|
2032
|
-
category,
|
|
2033
|
-
rule: newRule,
|
|
2034
|
-
};
|
|
2035
|
-
return {
|
|
2036
|
-
content: [{ type: 'text', text: JSON.stringify(output, null, 2) }]
|
|
2037
|
-
};
|
|
2038
|
-
});
|
|
2039
|
-
// Delete rule tool
|
|
2040
|
-
server.registerTool('delete_rule', {
|
|
2041
|
-
title: 'Delete Rule',
|
|
2042
|
-
description: 'Delete a project rule from the brainfile by category and ID',
|
|
2043
|
-
inputSchema: {
|
|
2044
|
-
file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
|
|
2045
|
-
category: zod_1.z.enum(['always', 'never', 'prefer', 'context']).describe('Rule category'),
|
|
2046
|
-
id: zod_1.z.number().describe('Rule ID to delete')
|
|
2047
|
-
}
|
|
2048
|
-
}, async ({ file, category, id }) => {
|
|
2049
|
-
const filePath = file || defaultFile;
|
|
2050
|
-
const result = readBoard(filePath);
|
|
2051
|
-
if ('error' in result) {
|
|
2052
|
-
return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
|
|
2053
|
-
}
|
|
2054
|
-
let { board } = result;
|
|
2055
|
-
// Find the rule being deleted for the response
|
|
2056
|
-
const existingRules = board.rules?.[category] || [];
|
|
2057
|
-
const ruleToDelete = existingRules.find((r) => r.id === id);
|
|
2058
|
-
if (!ruleToDelete) {
|
|
2059
|
-
const availableIds = existingRules.map((r) => r.id).join(', ');
|
|
2060
|
-
return {
|
|
2061
|
-
content: [{
|
|
2062
|
-
type: 'text',
|
|
2063
|
-
text: `Error: Rule ${id} not found in ${category}. ${availableIds ? `Available IDs: ${availableIds}` : `No rules in ${category} category`}`
|
|
2064
|
-
}],
|
|
2065
|
-
isError: true
|
|
2066
|
-
};
|
|
2067
|
-
}
|
|
2068
|
-
const deleteResult = (0, core_1.deleteRule)(board, category, id);
|
|
2069
|
-
if (!deleteResult.success || !deleteResult.board) {
|
|
2070
|
-
return { content: [{ type: 'text', text: `Error: ${deleteResult.error}` }], isError: true };
|
|
2071
|
-
}
|
|
2072
|
-
writeBoard(filePath, deleteResult.board);
|
|
2073
|
-
const output = {
|
|
2074
|
-
success: true,
|
|
2075
|
-
category,
|
|
2076
|
-
deletedRule: ruleToDelete,
|
|
2077
|
-
};
|
|
2078
|
-
return {
|
|
2079
|
-
content: [{ type: 'text', text: JSON.stringify(output, null, 2) }]
|
|
2080
|
-
};
|
|
2081
|
-
});
|
|
2082
|
-
// ==========================================================================
|
|
2083
|
-
// LEDGER
|
|
2084
|
-
// ==========================================================================
|
|
2085
|
-
server.registerTool('get_task_context', {
|
|
2086
|
-
title: 'Get Task Context',
|
|
2087
|
-
description: 'Get scoped recent ledger history for a task based on relatedFiles and contract deliverables',
|
|
2088
|
-
inputSchema: {
|
|
2089
|
-
file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
|
|
2090
|
-
taskId: zod_1.z.string().optional().describe('Task ID to scope context for'),
|
|
2091
|
-
maxEntries: zod_1.z.number().optional().describe('Maximum matching entries to return (default: 5)'),
|
|
2092
|
-
maxAgeDays: zod_1.z.number().optional().describe('Maximum age window in days (default: 90)'),
|
|
2093
|
-
task_id: zod_1.z.string().optional().describe('Alias of taskId'),
|
|
2094
|
-
max_entries: zod_1.z.number().optional().describe('Alias of maxEntries'),
|
|
2095
|
-
max_age_days: zod_1.z.number().optional().describe('Alias of maxAgeDays'),
|
|
2096
|
-
}
|
|
2097
|
-
}, async ({ file, taskId, maxEntries, maxAgeDays, task_id, max_entries, max_age_days }) => {
|
|
2098
|
-
const filePath = file || defaultFile;
|
|
2099
|
-
const resolvedTaskId = taskId || task_id;
|
|
2100
|
-
if (!resolvedTaskId) {
|
|
2101
|
-
return { content: [{ type: 'text', text: 'Error: taskId is required' }], isError: true };
|
|
2102
|
-
}
|
|
2103
|
-
let relatedFiles = [];
|
|
2104
|
-
let deliverables;
|
|
2105
|
-
if ((0, v2_detect_1.isV2)(filePath)) {
|
|
2106
|
-
const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
|
|
2107
|
-
const found = (0, v2_detect_1.findV2Task)(dirs, resolvedTaskId, true);
|
|
2108
|
-
if (!found) {
|
|
2109
|
-
return { content: [{ type: 'text', text: `Error: Task not found: ${resolvedTaskId}` }], isError: true };
|
|
2110
|
-
}
|
|
2111
|
-
relatedFiles = found.doc.task.relatedFiles || [];
|
|
2112
|
-
deliverables = found.doc.task.contract?.deliverables;
|
|
2113
|
-
}
|
|
2114
|
-
else {
|
|
2115
|
-
const boardResult = readBoard(filePath);
|
|
2116
|
-
if ('error' in boardResult) {
|
|
2117
|
-
return { content: [{ type: 'text', text: `Error: ${boardResult.error}` }], isError: true };
|
|
2118
|
-
}
|
|
2119
|
-
const taskInfo = (0, core_1.findTaskById)(boardResult.board, resolvedTaskId);
|
|
2120
|
-
if (!taskInfo) {
|
|
2121
|
-
return { content: [{ type: 'text', text: `Error: Task not found: ${resolvedTaskId}` }], isError: true };
|
|
2122
|
-
}
|
|
2123
|
-
relatedFiles = taskInfo.task.relatedFiles || [];
|
|
2124
|
-
deliverables = taskInfo.task.contract?.deliverables;
|
|
2125
|
-
}
|
|
2126
|
-
const limit = normalizePositiveInt(maxEntries ?? max_entries, 5);
|
|
2127
|
-
const ageDays = normalizeNonNegativeInt(maxAgeDays ?? max_age_days, 90);
|
|
2128
|
-
const since = toIsoDaysAgo(ageDays);
|
|
2129
|
-
const logsDir = resolveLedgerLogsDir(filePath);
|
|
2130
|
-
const entries = (0, dist_1.getTaskContext)(logsDir, relatedFiles, deliverables, {
|
|
2131
|
-
limit,
|
|
2132
|
-
dateRange: { from: since },
|
|
2133
|
-
});
|
|
2134
|
-
const scopeFiles = new Set();
|
|
2135
|
-
for (const value of relatedFiles) {
|
|
2136
|
-
const trimmed = value.trim();
|
|
2137
|
-
if (trimmed) {
|
|
2138
|
-
scopeFiles.add(trimmed);
|
|
2139
|
-
}
|
|
2140
|
-
}
|
|
2141
|
-
for (const deliverable of deliverables || []) {
|
|
2142
|
-
const rawPath = typeof deliverable === 'string' ? deliverable : deliverable.path;
|
|
2143
|
-
const trimmed = rawPath.trim();
|
|
2144
|
-
if (trimmed) {
|
|
2145
|
-
scopeFiles.add(trimmed);
|
|
2146
|
-
}
|
|
2147
|
-
}
|
|
2148
|
-
const output = {
|
|
2149
|
-
taskId: resolvedTaskId,
|
|
2150
|
-
count: entries.length,
|
|
2151
|
-
maxEntries: limit,
|
|
2152
|
-
maxAgeDays: ageDays,
|
|
2153
|
-
since,
|
|
2154
|
-
scopeFiles: Array.from(scopeFiles),
|
|
2155
|
-
entries,
|
|
2156
|
-
};
|
|
2157
|
-
return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
|
|
2158
|
-
});
|
|
2159
|
-
server.registerTool('query_ledger', {
|
|
2160
|
-
title: 'Query Ledger',
|
|
2161
|
-
description: 'Query ledger records by assignee, tags, date range, files, and contract status',
|
|
2162
|
-
inputSchema: {
|
|
2163
|
-
file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
|
|
2164
|
-
assignee: zod_1.z.string().optional().describe('Filter by assignee'),
|
|
2165
|
-
tags: zod_1.z.array(zod_1.z.string()).optional().describe('Filter by tags (OR match)'),
|
|
2166
|
-
since: zod_1.z.string().optional().describe('Filter completions since this ISO date/time'),
|
|
2167
|
-
until: zod_1.z.string().optional().describe('Filter completions until this ISO date/time'),
|
|
2168
|
-
files: zod_1.z.array(zod_1.z.string()).optional().describe('Filter by file paths touched'),
|
|
2169
|
-
contractStatus: zod_1.z.union([
|
|
2170
|
-
zod_1.z.enum(LEDGER_CONTRACT_STATUSES),
|
|
2171
|
-
zod_1.z.array(zod_1.z.enum(LEDGER_CONTRACT_STATUSES)),
|
|
2172
|
-
]).optional().describe('Filter by contract status'),
|
|
2173
|
-
contract_status: zod_1.z.union([
|
|
2174
|
-
zod_1.z.enum(LEDGER_CONTRACT_STATUSES),
|
|
2175
|
-
zod_1.z.array(zod_1.z.enum(LEDGER_CONTRACT_STATUSES)),
|
|
2176
|
-
]).optional().describe('Alias of contractStatus'),
|
|
2177
|
-
}
|
|
2178
|
-
}, async ({ file, assignee, tags, since, until, files, contractStatus, contract_status }) => {
|
|
2179
|
-
const filePath = file || defaultFile;
|
|
2180
|
-
const logsDir = resolveLedgerLogsDir(filePath);
|
|
2181
|
-
const contractFilter = contractStatus ?? contract_status;
|
|
2182
|
-
const dateRange = (since || until) ? { from: since, to: until } : undefined;
|
|
2183
|
-
const records = (0, dist_1.queryLedger)(logsDir, {
|
|
2184
|
-
assignee,
|
|
2185
|
-
tags,
|
|
2186
|
-
dateRange,
|
|
2187
|
-
files,
|
|
2188
|
-
contractStatus: contractFilter,
|
|
2189
|
-
});
|
|
2190
|
-
const output = {
|
|
2191
|
-
count: records.length,
|
|
2192
|
-
filters: {
|
|
2193
|
-
assignee,
|
|
2194
|
-
tags,
|
|
2195
|
-
since,
|
|
2196
|
-
until,
|
|
2197
|
-
files,
|
|
2198
|
-
contractStatus: contractFilter,
|
|
2199
|
-
},
|
|
2200
|
-
records,
|
|
2201
|
-
};
|
|
2202
|
-
return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
|
|
2203
|
-
});
|
|
2204
|
-
server.registerTool('get_stats', {
|
|
2205
|
-
title: 'Get Stats',
|
|
2206
|
-
description: 'Get aggregate completion analytics from the ledger with optional filters',
|
|
2207
|
-
inputSchema: {
|
|
2208
|
-
file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
|
|
2209
|
-
assignee: zod_1.z.string().optional().describe('Filter analytics by assignee'),
|
|
2210
|
-
since: zod_1.z.string().optional().describe('Only include completions since this ISO date/time'),
|
|
2211
|
-
tag: zod_1.z.string().optional().describe('Filter analytics by a single tag'),
|
|
2212
|
-
}
|
|
2213
|
-
}, async ({ file, assignee, since, tag }) => {
|
|
2214
|
-
const filePath = file || defaultFile;
|
|
2215
|
-
const logsDir = resolveLedgerLogsDir(filePath);
|
|
2216
|
-
const records = (0, dist_1.queryLedger)(logsDir, {
|
|
2217
|
-
assignee,
|
|
2218
|
-
tags: tag ? [tag] : undefined,
|
|
2219
|
-
dateRange: since ? { from: since } : undefined,
|
|
2220
|
-
});
|
|
2221
|
-
const cycleTimes = records
|
|
2222
|
-
.map((record) => record.cycleTimeHours)
|
|
2223
|
-
.filter((hours) => typeof hours === 'number' && Number.isFinite(hours) && hours >= 0);
|
|
2224
|
-
const cycleTimeTotal = Number(cycleTimes.reduce((sum, value) => sum + value, 0).toFixed(3));
|
|
2225
|
-
const cycleTimeAverage = cycleTimes.length > 0
|
|
2226
|
-
? Number((cycleTimeTotal / cycleTimes.length).toFixed(3))
|
|
2227
|
-
: 0;
|
|
2228
|
-
const cycleTimeMin = cycleTimes.length > 0 ? Number(Math.min(...cycleTimes).toFixed(3)) : 0;
|
|
2229
|
-
const cycleTimeMax = cycleTimes.length > 0 ? Number(Math.max(...cycleTimes).toFixed(3)) : 0;
|
|
2230
|
-
const byType = {};
|
|
2231
|
-
const byContractStatus = {};
|
|
2232
|
-
const byAssignee = {};
|
|
2233
|
-
const byTag = {};
|
|
2234
|
-
const fileCounts = new Map();
|
|
2235
|
-
let firstCompletionAt = null;
|
|
2236
|
-
let lastCompletionAt = null;
|
|
2237
|
-
let firstCompletionMs = null;
|
|
2238
|
-
let lastCompletionMs = null;
|
|
2239
|
-
for (const record of records) {
|
|
2240
|
-
byType[record.type] = (byType[record.type] || 0) + 1;
|
|
2241
|
-
if (record.contractStatus) {
|
|
2242
|
-
byContractStatus[record.contractStatus] = (byContractStatus[record.contractStatus] || 0) + 1;
|
|
2243
|
-
}
|
|
2244
|
-
if (record.assignee) {
|
|
2245
|
-
byAssignee[record.assignee] = (byAssignee[record.assignee] || 0) + 1;
|
|
2246
|
-
}
|
|
2247
|
-
for (const recordTag of record.tags || []) {
|
|
2248
|
-
byTag[recordTag] = (byTag[recordTag] || 0) + 1;
|
|
2249
|
-
}
|
|
2250
|
-
for (const changedFile of record.filesChanged || []) {
|
|
2251
|
-
fileCounts.set(changedFile, (fileCounts.get(changedFile) || 0) + 1);
|
|
2252
|
-
}
|
|
2253
|
-
const completedMs = Date.parse(record.completedAt);
|
|
2254
|
-
if (Number.isFinite(completedMs)) {
|
|
2255
|
-
if (firstCompletionMs === null || completedMs < firstCompletionMs) {
|
|
2256
|
-
firstCompletionMs = completedMs;
|
|
2257
|
-
firstCompletionAt = record.completedAt;
|
|
2258
|
-
}
|
|
2259
|
-
if (lastCompletionMs === null || completedMs > lastCompletionMs) {
|
|
2260
|
-
lastCompletionMs = completedMs;
|
|
2261
|
-
lastCompletionAt = record.completedAt;
|
|
2262
|
-
}
|
|
2263
|
-
}
|
|
2264
|
-
}
|
|
2265
|
-
const topFiles = Array.from(fileCounts.entries())
|
|
2266
|
-
.sort((a, b) => b[1] - a[1])
|
|
2267
|
-
.slice(0, 10)
|
|
2268
|
-
.map(([filePath, count]) => ({ filePath, count }));
|
|
2269
|
-
const output = {
|
|
2270
|
-
filters: { assignee, since, tag },
|
|
2271
|
-
totals: {
|
|
2272
|
-
completed: records.length,
|
|
2273
|
-
uniqueFilesTouched: fileCounts.size,
|
|
2274
|
-
},
|
|
2275
|
-
cycleTimeHours: {
|
|
2276
|
-
total: cycleTimeTotal,
|
|
2277
|
-
average: cycleTimeAverage,
|
|
2278
|
-
median: median(cycleTimes),
|
|
2279
|
-
min: cycleTimeMin,
|
|
2280
|
-
max: cycleTimeMax,
|
|
2281
|
-
},
|
|
2282
|
-
breakdown: {
|
|
2283
|
-
byType,
|
|
2284
|
-
byContractStatus,
|
|
2285
|
-
byAssignee,
|
|
2286
|
-
byTag,
|
|
2287
|
-
},
|
|
2288
|
-
topFiles,
|
|
2289
|
-
window: {
|
|
2290
|
-
firstCompletionAt,
|
|
2291
|
-
lastCompletionAt,
|
|
2292
|
-
},
|
|
2293
|
-
};
|
|
2294
|
-
return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
|
|
2295
|
-
});
|
|
2296
|
-
server.registerTool('get_file_history', {
|
|
2297
|
-
title: 'Get File History',
|
|
2298
|
-
description: 'Get recent completed records that touched a specific file path',
|
|
2299
|
-
inputSchema: {
|
|
2300
|
-
file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
|
|
2301
|
-
filePath: zod_1.z.string().optional().describe('File path to query'),
|
|
2302
|
-
last: zod_1.z.number().optional().describe('Maximum records to return (default: 10)'),
|
|
2303
|
-
since: zod_1.z.string().optional().describe('Only include completions since this ISO date/time'),
|
|
2304
|
-
file_path: zod_1.z.string().optional().describe('Alias of filePath'),
|
|
2305
|
-
}
|
|
2306
|
-
}, async ({ file, filePath: targetFilePath, last, since, file_path }) => {
|
|
2307
|
-
const filePath = file || defaultFile;
|
|
2308
|
-
const resolvedFilePath = targetFilePath || file_path;
|
|
2309
|
-
if (!resolvedFilePath) {
|
|
2310
|
-
return { content: [{ type: 'text', text: 'Error: filePath is required' }], isError: true };
|
|
2311
|
-
}
|
|
2312
|
-
const logsDir = resolveLedgerLogsDir(filePath);
|
|
2313
|
-
const limit = normalizePositiveInt(last, 10);
|
|
2314
|
-
const records = (0, dist_1.getFileHistory)(logsDir, resolvedFilePath, {
|
|
2315
|
-
limit,
|
|
2316
|
-
dateRange: since ? { from: since } : undefined,
|
|
2317
|
-
});
|
|
2318
|
-
const output = {
|
|
2319
|
-
filePath: resolvedFilePath,
|
|
2320
|
-
count: records.length,
|
|
2321
|
-
last: limit,
|
|
2322
|
-
since,
|
|
2323
|
-
records,
|
|
2324
|
-
};
|
|
2325
|
-
return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
|
|
2326
|
-
});
|
|
2327
|
-
// ==========================================================================
|
|
2328
|
-
// V2 NEW TOOLS: complete_task, search_logs, append_log
|
|
2329
|
-
// ==========================================================================
|
|
2330
|
-
// Complete task tool
|
|
2331
|
-
server.registerTool('complete_task', {
|
|
2332
|
-
title: 'Complete Task',
|
|
2333
|
-
description: 'Complete a task - in v2, moves task file from board/ to logs/ with completedAt timestamp. In v1, moves to done column.',
|
|
2334
|
-
inputSchema: {
|
|
2335
|
-
file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
|
|
2336
|
-
task: zod_1.z.string().describe('Task ID to complete'),
|
|
2337
|
-
summary: zod_1.z.string().optional().describe('Completion summary (default: Completed: {title})'),
|
|
2338
|
-
filesChanged: zod_1.z.array(zod_1.z.string()).optional().describe('Files changed during implementation (optional; inferred from git diff if omitted)'),
|
|
2339
|
-
files_changed: zod_1.z.array(zod_1.z.string()).optional().describe('Alias of filesChanged'),
|
|
2340
|
-
}
|
|
2341
|
-
}, async ({ file, task, summary, filesChanged, files_changed }) => {
|
|
2342
|
-
const filePath = file || defaultFile;
|
|
2343
|
-
if ((0, v2_detect_1.isV2)(filePath)) {
|
|
2344
|
-
const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
|
|
2345
|
-
const taskPath = path.join(dirs.boardDir, (0, core_3.taskFileName)(task));
|
|
2346
|
-
const doc = (0, core_3.readTaskFile)(taskPath);
|
|
2347
|
-
if (!doc) {
|
|
2348
|
-
return { content: [{ type: 'text', text: `Error: Task not found: ${task}` }], isError: true };
|
|
2349
|
-
}
|
|
2350
|
-
const resolvedSummary = summary?.trim() || `Completed: ${doc.task.title}`;
|
|
2351
|
-
const rawFiles = filesChanged ?? files_changed ?? [];
|
|
2352
|
-
const providedFiles = rawFiles
|
|
2353
|
-
.map((entry) => entry.trim())
|
|
2354
|
-
.filter((entry) => entry.length > 0);
|
|
2355
|
-
const inferredFiles = providedFiles.length > 0 ? providedFiles : inferFilesChangedFromGit(filePath);
|
|
2356
|
-
const filesSource = providedFiles.length > 0
|
|
2357
|
-
? 'input'
|
|
2358
|
-
: inferredFiles.length > 0
|
|
2359
|
-
? 'git_diff'
|
|
2360
|
-
: 'core_default';
|
|
2361
|
-
const completeResult = (0, dist_1.completeTaskFile)(taskPath, dirs.logsDir, {
|
|
2362
|
-
summary: resolvedSummary,
|
|
2363
|
-
filesChanged: inferredFiles.length > 0 ? inferredFiles : undefined,
|
|
2364
|
-
});
|
|
2365
|
-
if (!completeResult.success || !completeResult.task) {
|
|
2366
|
-
return { content: [{ type: 'text', text: `Error: ${completeResult.error || `Failed to complete task: ${task}`}` }], isError: true };
|
|
2367
|
-
}
|
|
2368
|
-
const output = {
|
|
2369
|
-
task,
|
|
2370
|
-
completedAt: completeResult.task.completedAt,
|
|
2371
|
-
summary: resolvedSummary,
|
|
2372
|
-
filesChanged: inferredFiles,
|
|
2373
|
-
filesSource,
|
|
2374
|
-
};
|
|
2375
|
-
return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
|
|
2376
|
-
}
|
|
2377
|
-
try {
|
|
2378
|
-
const { completeCommand } = await Promise.resolve().then(() => __importStar(require('./complete')));
|
|
2379
|
-
const result = completeCommand({ file: filePath, task }, { log: () => { }, warn: () => { }, error: () => { }, info: () => { } });
|
|
2380
|
-
const hasLedgerOnlyParams = Boolean(summary) || ((filesChanged ?? files_changed)?.length || 0) > 0;
|
|
2381
|
-
return {
|
|
2382
|
-
content: [{
|
|
2383
|
-
type: 'text',
|
|
2384
|
-
text: `Task ${task} completed at ${result.completedAt}${hasLedgerOnlyParams ? `\nNote: summary/filesChanged are only applied in v2 ledger mode.` : ''}`
|
|
2385
|
-
}]
|
|
2386
|
-
};
|
|
1748
|
+
return { content: [{ type: 'text', text: `Task ${task} archived to Linear Issue ${linearResult.issueId} (Done)\n\nView: ${linearResult.issueUrl}` }] };
|
|
2387
1749
|
}
|
|
2388
1750
|
catch (e) {
|
|
2389
1751
|
return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
|
|
2390
1752
|
}
|
|
2391
1753
|
});
|
|
2392
|
-
// Search logs tool
|
|
2393
|
-
server.registerTool('search_logs', {
|
|
2394
|
-
title: 'Search Logs',
|
|
2395
|
-
description: 'Search across completed task logs. Requires v2 per-task file architecture.',
|
|
2396
|
-
inputSchema: {
|
|
2397
|
-
file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
|
|
2398
|
-
query: zod_1.z.string().optional().describe('Search query to match against log content'),
|
|
2399
|
-
recent: zod_1.z.boolean().optional().describe('List recently completed tasks'),
|
|
2400
|
-
task: zod_1.z.string().optional().describe('View a specific task log'),
|
|
2401
|
-
}
|
|
2402
|
-
}, async ({ file, query, recent, task: taskId }) => {
|
|
2403
|
-
const filePath = file || defaultFile;
|
|
2404
|
-
if (!(0, v2_detect_1.isV2)(filePath)) {
|
|
2405
|
-
return { content: [{ type: 'text', text: 'Error: search_logs requires v2 per-task file architecture. Run: brainfile migrate' }], isError: true };
|
|
2406
|
-
}
|
|
2407
|
-
const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
|
|
2408
|
-
// View specific task log
|
|
2409
|
-
if (taskId) {
|
|
2410
|
-
const found = (0, v2_detect_1.findV2Task)(dirs, taskId, true);
|
|
2411
|
-
if (!found) {
|
|
2412
|
-
return { content: [{ type: 'text', text: `Error: Task not found: ${taskId}` }], isError: true };
|
|
2413
|
-
}
|
|
2414
|
-
const output = {
|
|
2415
|
-
id: found.doc.task.id,
|
|
2416
|
-
title: found.doc.task.title,
|
|
2417
|
-
completedAt: found.doc.task.completedAt,
|
|
2418
|
-
description: (0, v2_detect_1.extractDescription)(found.doc.body),
|
|
2419
|
-
log: (0, v2_detect_1.extractLog)(found.doc.body),
|
|
2420
|
-
};
|
|
2421
|
-
return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
|
|
2422
|
-
}
|
|
2423
|
-
// Search logs
|
|
2424
|
-
if (query) {
|
|
2425
|
-
const logDocs = (0, core_3.readTasksDir)(dirs.logsDir);
|
|
2426
|
-
const queryLower = query.toLowerCase();
|
|
2427
|
-
const matches = [];
|
|
2428
|
-
for (const doc of logDocs) {
|
|
2429
|
-
const task = doc.task;
|
|
2430
|
-
const desc = (0, v2_detect_1.extractDescription)(doc.body) || '';
|
|
2431
|
-
const log = (0, v2_detect_1.extractLog)(doc.body) || '';
|
|
2432
|
-
const fullText = [task.title, task.description || '', desc, log].join(' ').toLowerCase();
|
|
2433
|
-
if (fullText.includes(queryLower)) {
|
|
2434
|
-
matches.push({ id: task.id, title: task.title, completedAt: task.completedAt });
|
|
2435
|
-
}
|
|
2436
|
-
}
|
|
2437
|
-
return { content: [{ type: 'text', text: JSON.stringify({ results: matches, count: matches.length }, null, 2) }] };
|
|
2438
|
-
}
|
|
2439
|
-
// Recent logs (default)
|
|
2440
|
-
const logDocs = (0, core_3.readTasksDir)(dirs.logsDir);
|
|
2441
|
-
logDocs.sort((a, b) => (b.task.completedAt || '').localeCompare(a.task.completedAt || ''));
|
|
2442
|
-
const recent20 = logDocs.slice(0, 20).map(doc => ({
|
|
2443
|
-
id: doc.task.id,
|
|
2444
|
-
title: doc.task.title,
|
|
2445
|
-
completedAt: doc.task.completedAt,
|
|
2446
|
-
}));
|
|
2447
|
-
return { content: [{ type: 'text', text: JSON.stringify({ logs: recent20, count: recent20.length }, null, 2) }] };
|
|
2448
|
-
});
|
|
2449
|
-
// Append log tool
|
|
2450
|
-
server.registerTool('append_log', {
|
|
2451
|
-
title: 'Append Log',
|
|
2452
|
-
description: 'Append a timestamped entry to a task log section. Works on both active tasks and completed logs. Requires v2 per-task file architecture.',
|
|
2453
|
-
inputSchema: {
|
|
2454
|
-
file: zod_1.z.string().optional().describe('Path to brainfile.md (default: brainfile.md)'),
|
|
2455
|
-
task: zod_1.z.string().describe('Task ID to append log to'),
|
|
2456
|
-
message: zod_1.z.string().describe('Log message to append'),
|
|
2457
|
-
agent: zod_1.z.string().optional().describe('Agent name for attribution'),
|
|
2458
|
-
}
|
|
2459
|
-
}, async ({ file, task: taskId, message, agent }) => {
|
|
2460
|
-
const filePath = file || defaultFile;
|
|
2461
|
-
if (!(0, v2_detect_1.isV2)(filePath)) {
|
|
2462
|
-
return { content: [{ type: 'text', text: 'Error: append_log requires v2 per-task file architecture. Run: brainfile migrate' }], isError: true };
|
|
2463
|
-
}
|
|
2464
|
-
const dirs = (0, v2_detect_1.getV2Dirs)(filePath);
|
|
2465
|
-
const found = (0, v2_detect_1.findV2Task)(dirs, taskId, true);
|
|
2466
|
-
if (!found) {
|
|
2467
|
-
return { content: [{ type: 'text', text: `Error: Task not found: ${taskId}` }], isError: true };
|
|
2468
|
-
}
|
|
2469
|
-
const { doc, filePath: taskFilePath } = found;
|
|
2470
|
-
const timestamp = new Date().toISOString();
|
|
2471
|
-
const agentPrefix = agent ? `[${agent}] ` : '';
|
|
2472
|
-
const entry = `- ${timestamp}: ${agentPrefix}${message}`;
|
|
2473
|
-
const existingDescription = (0, v2_detect_1.extractDescription)(doc.body);
|
|
2474
|
-
const existingLog = (0, v2_detect_1.extractLog)(doc.body) || '';
|
|
2475
|
-
const newLog = existingLog ? `${existingLog}\n${entry}` : entry;
|
|
2476
|
-
const newBody = (0, v2_detect_1.composeBody)(existingDescription, newLog);
|
|
2477
|
-
(0, core_3.writeTaskFile)(taskFilePath, doc.task, newBody);
|
|
2478
|
-
return { content: [{ type: 'text', text: `Log entry added to ${taskId}: ${entry}` }] };
|
|
2479
|
-
});
|
|
2480
1754
|
// Connect via stdio
|
|
2481
1755
|
const transport = new stdio_js_1.StdioServerTransport();
|
|
2482
1756
|
await server.connect(transport);
|