@claudetools/tools 0.3.9 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/codedna/generators/base.d.ts +41 -0
- package/dist/codedna/generators/base.js +102 -0
- package/dist/codedna/generators/express-api.d.ts +12 -0
- package/dist/codedna/generators/express-api.js +61 -0
- package/dist/codedna/index.d.ts +4 -0
- package/dist/codedna/index.js +7 -0
- package/dist/codedna/parser.d.ts +80 -0
- package/dist/codedna/parser.js +176 -0
- package/dist/codedna/registry.d.ts +60 -0
- package/dist/codedna/registry.js +214 -0
- package/dist/codedna/template-engine.d.ts +17 -0
- package/dist/codedna/template-engine.js +149 -0
- package/dist/codedna/types.d.ts +64 -0
- package/dist/codedna/types.js +4 -0
- package/dist/handlers/codedna-handlers.d.ts +122 -0
- package/dist/handlers/codedna-handlers.js +167 -0
- package/dist/handlers/tool-handlers.js +593 -14
- package/dist/helpers/api-client.d.ts +37 -0
- package/dist/helpers/api-client.js +63 -0
- package/dist/helpers/library-detection.d.ts +26 -0
- package/dist/helpers/library-detection.js +145 -0
- package/dist/helpers/tasks-retry.d.ts +49 -0
- package/dist/helpers/tasks-retry.js +168 -0
- package/dist/helpers/tasks.d.ts +24 -1
- package/dist/helpers/tasks.js +146 -50
- package/dist/helpers/workers.d.ts +25 -0
- package/dist/helpers/workers.js +80 -0
- package/dist/templates/claude-md.d.ts +1 -1
- package/dist/templates/claude-md.js +16 -5
- package/dist/tools.js +314 -0
- package/package.json +3 -1
|
@@ -3,13 +3,16 @@
|
|
|
3
3
|
// =============================================================================
|
|
4
4
|
import { CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
5
5
|
import { mcpLogger } from '../logger.js';
|
|
6
|
-
import { getDefaultProjectId, DEFAULT_USER_ID, lastContextUsed, setLastContextUsed } from '../helpers/config.js';
|
|
6
|
+
import { getDefaultProjectId, DEFAULT_USER_ID, lastContextUsed, setLastContextUsed, API_BASE_URL } from '../helpers/config.js';
|
|
7
7
|
import { EXPERT_WORKERS, matchTaskToWorker } from '../helpers/workers.js';
|
|
8
|
-
import { searchMemory, addMemory, storeFact, getContext, getSummary, getEntities, injectContext, apiRequest } from '../helpers/api-client.js';
|
|
8
|
+
import { searchMemory, addMemory, storeFact, getContext, getSummary, getEntities, injectContext, apiRequest, listCachedDocs, getCachedDocs, cacheDocs } from '../helpers/api-client.js';
|
|
9
9
|
import { queryDependencies, analyzeImpact } from '../helpers/dependencies.js';
|
|
10
10
|
import { checkPatterns } from '../helpers/patterns.js';
|
|
11
11
|
import { formatContextForClaude } from '../helpers/formatter.js';
|
|
12
|
-
import { createTask, listTasks, getTask, claimTask, releaseTask, updateTaskStatus, addTaskContext, getTaskSummary, heartbeatTask, parseJsonArray, getDispatchableTasks, getExecutionContext, resolveTaskDependencies, } from '../helpers/tasks.js';
|
|
12
|
+
import { createTask, listTasks, getTask, claimTask, releaseTask, updateTaskStatus, addTaskContext, getTaskSummary, heartbeatTask, parseJsonArray, getDispatchableTasks, getExecutionContext, resolveTaskDependencies, getEpicStatus, getActiveTaskCount, } from '../helpers/tasks.js';
|
|
13
|
+
import { detectTimedOutTasks, retryTask, failTask, autoRetryTimedOutTasks, } from '../helpers/tasks-retry.js';
|
|
14
|
+
import { detectLibrariesFromPlan } from '../helpers/library-detection.js';
|
|
15
|
+
import { handleGenerateApi, handleGenerateFrontend, handleGenerateComponent, handleListGenerators, handleValidateSpec, } from './codedna-handlers.js';
|
|
13
16
|
export function registerToolHandlers(server) {
|
|
14
17
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
15
18
|
const { name, arguments: args } = request.params;
|
|
@@ -344,6 +347,20 @@ export function registerToolHandlers(server) {
|
|
|
344
347
|
const risks = args?.risks;
|
|
345
348
|
const questions = args?.questions;
|
|
346
349
|
const priority = args?.priority || 'medium';
|
|
350
|
+
// Detect libraries mentioned in the plan
|
|
351
|
+
const detectedLibraries = detectLibrariesFromPlan(goal, tasks);
|
|
352
|
+
// Check docs cache for each detected library
|
|
353
|
+
const cachedDocsStatus = [];
|
|
354
|
+
const cachedDocsList = await listCachedDocs(projectId);
|
|
355
|
+
const cachedLibraryIds = new Set(cachedDocsList.libraries.map(l => l.library_id));
|
|
356
|
+
for (const lib of detectedLibraries) {
|
|
357
|
+
const isCached = lib.context7Id ? cachedLibraryIds.has(lib.context7Id) : false;
|
|
358
|
+
cachedDocsStatus.push({
|
|
359
|
+
name: lib.name,
|
|
360
|
+
cached: isCached,
|
|
361
|
+
context7Id: lib.context7Id,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
347
364
|
mcpLogger.toolResult(name, true, timer());
|
|
348
365
|
let output = `## Plan: ${epicTitle}\n\n`;
|
|
349
366
|
output += `**Goal:** ${goal}\n\n`;
|
|
@@ -353,6 +370,27 @@ export function registerToolHandlers(server) {
|
|
|
353
370
|
if (approach) {
|
|
354
371
|
output += `**Approach:** ${approach}\n\n`;
|
|
355
372
|
}
|
|
373
|
+
// Show detected libraries with cache status
|
|
374
|
+
if (cachedDocsStatus.length > 0) {
|
|
375
|
+
const needsFetch = cachedDocsStatus.filter(l => !l.cached && l.context7Id);
|
|
376
|
+
const alreadyCached = cachedDocsStatus.filter(l => l.cached);
|
|
377
|
+
const noDocsAvailable = cachedDocsStatus.filter(l => !l.context7Id);
|
|
378
|
+
output += `### Documentation Status\n\n`;
|
|
379
|
+
if (alreadyCached.length > 0) {
|
|
380
|
+
output += `**Cached:** ${alreadyCached.map(l => l.name).join(', ')}\n`;
|
|
381
|
+
}
|
|
382
|
+
if (needsFetch.length > 0) {
|
|
383
|
+
output += `**Needs fetching:** ${needsFetch.map(l => l.name).join(', ')}\n`;
|
|
384
|
+
output += `\n> **Action Required:** Fetch docs from context7, then cache them:\n`;
|
|
385
|
+
output += `> 1. Use \`resolve-library-id\` and \`get-library-docs\` for each: ${needsFetch.map(l => `\`${l.context7Id}\``).join(', ')}\n`;
|
|
386
|
+
output += `> 2. Cache each with \`docs_cache(library_id, library_name, content)\`\n`;
|
|
387
|
+
output += `> 3. Then approve the plan to create tasks with docs attached\n`;
|
|
388
|
+
}
|
|
389
|
+
if (noDocsAvailable.length > 0) {
|
|
390
|
+
output += `**No docs available:** ${noDocsAvailable.map(l => l.name).join(', ')}\n`;
|
|
391
|
+
}
|
|
392
|
+
output += '\n';
|
|
393
|
+
}
|
|
356
394
|
output += `### Tasks\n\n`;
|
|
357
395
|
tasks.forEach((t, i) => {
|
|
358
396
|
const effort = t.effort ? ` (${t.effort})` : '';
|
|
@@ -412,9 +450,63 @@ export function registerToolHandlers(server) {
|
|
|
412
450
|
agent_type: worker.id,
|
|
413
451
|
domain: taskDef.domain,
|
|
414
452
|
});
|
|
415
|
-
|
|
453
|
+
const taskId = taskResult.data.id;
|
|
454
|
+
let contextCount = 0;
|
|
455
|
+
// Search memory for relevant patterns/facts
|
|
456
|
+
const searchQuery = `${taskDef.title} ${taskDef.description || ''}`;
|
|
457
|
+
try {
|
|
458
|
+
const memoryResults = await searchMemory(projectId, searchQuery, 5);
|
|
459
|
+
// Attach relevant facts as context
|
|
460
|
+
if (memoryResults.relevant_facts?.length > 0) {
|
|
461
|
+
const factsContent = memoryResults.relevant_facts
|
|
462
|
+
.slice(0, 3) // Limit to top 3 facts
|
|
463
|
+
.map(f => `- ${f.fact}`)
|
|
464
|
+
.join('\n');
|
|
465
|
+
await addTaskContext(DEFAULT_USER_ID, projectId, taskId, 'pattern', factsContent, 'task_plan', 'memory:facts');
|
|
466
|
+
contextCount += memoryResults.relevant_facts.slice(0, 3).length;
|
|
467
|
+
}
|
|
468
|
+
// Attach relevant entities as context
|
|
469
|
+
if (memoryResults.relevant_entities?.length > 0) {
|
|
470
|
+
const entitiesContent = memoryResults.relevant_entities
|
|
471
|
+
.slice(0, 3)
|
|
472
|
+
.map(e => `- **${e.name}** (${e.labels.join(', ')})${e.summary ? `: ${e.summary}` : ''}`)
|
|
473
|
+
.join('\n');
|
|
474
|
+
await addTaskContext(DEFAULT_USER_ID, projectId, taskId, 'reference', entitiesContent, 'task_plan', 'memory:entities');
|
|
475
|
+
contextCount += memoryResults.relevant_entities.slice(0, 3).length;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
catch {
|
|
479
|
+
// Memory search failed, continue without context
|
|
480
|
+
mcpLogger.warn('TOOL', `Memory search failed for task: ${taskDef.title}`);
|
|
481
|
+
}
|
|
482
|
+
// Attach cached documentation for detected libraries
|
|
483
|
+
try {
|
|
484
|
+
const taskLibraries = detectLibrariesFromPlan(`${taskDef.title} ${taskDef.description || ''}`, [{ title: taskDef.title, description: taskDef.description }]);
|
|
485
|
+
for (const lib of taskLibraries) {
|
|
486
|
+
if (lib.context7Id) {
|
|
487
|
+
const cachedDoc = await getCachedDocs(projectId, lib.context7Id);
|
|
488
|
+
if (cachedDoc && cachedDoc.content) {
|
|
489
|
+
// Truncate to reasonable size for task context (first 2000 chars)
|
|
490
|
+
const truncatedContent = cachedDoc.content.length > 2000
|
|
491
|
+
? cachedDoc.content.slice(0, 2000) + '\n\n... (truncated, use docs_get for full content)'
|
|
492
|
+
: cachedDoc.content;
|
|
493
|
+
await addTaskContext(DEFAULT_USER_ID, projectId, taskId, 'reference', `## ${lib.name} Documentation\n\n${truncatedContent}`, 'task_plan', `docs:${lib.context7Id}`);
|
|
494
|
+
contextCount++;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
catch {
|
|
500
|
+
mcpLogger.warn('TOOL', `Docs attachment failed for task: ${taskDef.title}`);
|
|
501
|
+
}
|
|
502
|
+
// Immediately set task to 'ready' status for auto-dispatch
|
|
503
|
+
await updateTaskStatus(DEFAULT_USER_ID, projectId, taskId, 'ready');
|
|
504
|
+
taskResult.data.status = 'ready'; // Update local copy
|
|
505
|
+
createdTasks.push({ task: taskResult.data, worker, contextCount });
|
|
416
506
|
}
|
|
417
507
|
mcpLogger.toolResult(name, true, timer());
|
|
508
|
+
// Get dispatchable tasks with full execution context
|
|
509
|
+
const dispatchable = await getDispatchableTasks(DEFAULT_USER_ID, projectId, epic.id, createdTasks.length);
|
|
418
510
|
// Group tasks by worker for parallel dispatch overview
|
|
419
511
|
const byWorker = new Map();
|
|
420
512
|
for (const item of createdTasks) {
|
|
@@ -422,16 +514,22 @@ export function registerToolHandlers(server) {
|
|
|
422
514
|
existing.push(item);
|
|
423
515
|
byWorker.set(item.worker.id, existing);
|
|
424
516
|
}
|
|
425
|
-
|
|
517
|
+
// Calculate total context injected
|
|
518
|
+
const totalContext = createdTasks.reduce((sum, t) => sum + t.contextCount, 0);
|
|
519
|
+
let output = `# Work Plan Created & Ready for Dispatch\n\n`;
|
|
426
520
|
output += `## Epic: ${epic.title}\n`;
|
|
427
521
|
output += `**ID:** \`${epic.id}\`\n`;
|
|
428
|
-
output += `**Priority:** ${priority}\n
|
|
522
|
+
output += `**Priority:** ${priority}\n`;
|
|
523
|
+
output += `**Status:** All tasks set to \`ready\`\n`;
|
|
524
|
+
output += `**Context Injected:** ${totalContext} items from memory\n\n`;
|
|
429
525
|
output += `**Goal:** ${goal}\n\n`;
|
|
430
526
|
output += `## Tasks (${createdTasks.length})\n\n`;
|
|
431
527
|
createdTasks.forEach((item, i) => {
|
|
432
528
|
output += `${i + 1}. **${item.task.title}**\n`;
|
|
433
529
|
output += ` - ID: \`${item.task.id}\`\n`;
|
|
434
530
|
output += ` - Worker: ${item.worker.name} (\`${item.worker.id}\`)\n`;
|
|
531
|
+
output += ` - Status: \`ready\`\n`;
|
|
532
|
+
output += ` - Context: ${item.contextCount} items\n`;
|
|
435
533
|
if (item.task.estimated_effort)
|
|
436
534
|
output += ` - Effort: ${item.task.estimated_effort}\n`;
|
|
437
535
|
if (item.task.description)
|
|
@@ -443,11 +541,25 @@ export function registerToolHandlers(server) {
|
|
|
443
541
|
const worker = EXPERT_WORKERS[workerId];
|
|
444
542
|
output += `- **${worker.name}**: ${items.length} task(s)\n`;
|
|
445
543
|
}
|
|
544
|
+
// Auto-dispatch section with exact Task tool invocations
|
|
446
545
|
output += `\n---\n\n`;
|
|
447
|
-
output += `##
|
|
448
|
-
output += `**
|
|
449
|
-
output +=
|
|
450
|
-
|
|
546
|
+
output += `## Auto-Dispatch: Spawn Parallel Agents Now\n\n`;
|
|
547
|
+
output += `**IMPORTANT:** Execute these Task tool calls in a SINGLE message to spawn parallel workers:\n\n`;
|
|
548
|
+
output += `\`\`\`\n`;
|
|
549
|
+
for (const dt of dispatchable) {
|
|
550
|
+
// Build a concise but complete prompt for the agent
|
|
551
|
+
const taskPrompt = [
|
|
552
|
+
`Execute task ${dt.task.id}: ${dt.task.title}`,
|
|
553
|
+
dt.task.description ? `\nDescription: ${dt.task.description}` : '',
|
|
554
|
+
`\nEpic: ${epic.title}`,
|
|
555
|
+
`\nGoal: ${goal}`,
|
|
556
|
+
`\nWhen complete, call task_complete(task_id="${dt.task.id}", summary="...") with a summary of work done.`,
|
|
557
|
+
].filter(Boolean).join('');
|
|
558
|
+
output += `Task(subagent_type="general-purpose", prompt="${taskPrompt.replace(/"/g, '\\"')}")\n`;
|
|
559
|
+
}
|
|
560
|
+
output += `\`\`\`\n\n`;
|
|
561
|
+
output += `**Note:** All ${dispatchable.length} tasks are ready for parallel execution. `;
|
|
562
|
+
output += `Each agent will claim its task, execute, and report completion.\n`;
|
|
451
563
|
return {
|
|
452
564
|
content: [{ type: 'text', text: output }],
|
|
453
565
|
};
|
|
@@ -927,13 +1039,349 @@ export function registerToolHandlers(server) {
|
|
|
927
1039
|
content: [{ type: 'text', text: output }],
|
|
928
1040
|
};
|
|
929
1041
|
}
|
|
1042
|
+
case 'task_epic_status': {
|
|
1043
|
+
const epicId = args?.epic_id;
|
|
1044
|
+
const status = await getEpicStatus(DEFAULT_USER_ID, projectId, epicId);
|
|
1045
|
+
mcpLogger.toolResult(name, true, timer());
|
|
1046
|
+
const statusEmoji = {
|
|
1047
|
+
backlog: '📋', ready: '🟢', in_progress: '🔄',
|
|
1048
|
+
blocked: '🚫', review: '👀', done: '✅', cancelled: '❌'
|
|
1049
|
+
};
|
|
1050
|
+
let output = `# Epic Progress: ${status.epic.title}\n\n`;
|
|
1051
|
+
output += `**Epic ID:** \`${epicId}\`\n`;
|
|
1052
|
+
output += `**Epic Status:** ${status.epic.status}`;
|
|
1053
|
+
if (status.autoCompleted) {
|
|
1054
|
+
output += ` ✨ (auto-completed)`;
|
|
1055
|
+
}
|
|
1056
|
+
output += `\n`;
|
|
1057
|
+
output += `**Total Tasks:** ${status.totalTasks}\n`;
|
|
1058
|
+
output += `**Progress:** ${status.percentComplete}% complete\n`;
|
|
1059
|
+
output += `**All Complete:** ${status.allComplete ? '✅ Yes' : '❌ No'}\n\n`;
|
|
1060
|
+
output += `## Task Breakdown\n\n`;
|
|
1061
|
+
if (status.totalTasks === 0) {
|
|
1062
|
+
output += `No child tasks found for this epic.\n`;
|
|
1063
|
+
}
|
|
1064
|
+
else {
|
|
1065
|
+
output += `| Status | Count | Percentage |\n`;
|
|
1066
|
+
output += `|--------|-------|------------|\n`;
|
|
1067
|
+
for (const [statusKey, count] of Object.entries(status.byStatus)) {
|
|
1068
|
+
if (count > 0) {
|
|
1069
|
+
const emoji = statusEmoji[statusKey] || '📝';
|
|
1070
|
+
const percentage = Math.round((count / status.totalTasks) * 100);
|
|
1071
|
+
output += `| ${emoji} ${statusKey} | ${count} | ${percentage}% |\n`;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
output += `\n`;
|
|
1075
|
+
// Progress bar visualization
|
|
1076
|
+
const doneCount = status.byStatus.done || 0;
|
|
1077
|
+
const progressBar = '█'.repeat(Math.floor(status.percentComplete / 5)) +
|
|
1078
|
+
'░'.repeat(20 - Math.floor(status.percentComplete / 5));
|
|
1079
|
+
output += `**Progress Bar:** [${progressBar}] ${status.percentComplete}%\n\n`;
|
|
1080
|
+
// Summary
|
|
1081
|
+
if (status.allComplete) {
|
|
1082
|
+
output += `🎉 **All tasks completed!** Epic ${status.autoCompleted ? 'has been automatically marked as done' : 'is complete'}.`;
|
|
1083
|
+
}
|
|
1084
|
+
else {
|
|
1085
|
+
const remaining = status.totalTasks - doneCount;
|
|
1086
|
+
output += `📊 **${remaining} task(s) remaining** to complete this epic.`;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
return {
|
|
1090
|
+
content: [{ type: 'text', text: output }],
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
case 'task_detect_timeouts': {
|
|
1094
|
+
const timedOut = await detectTimedOutTasks(DEFAULT_USER_ID, projectId);
|
|
1095
|
+
mcpLogger.toolResult(name, true, timer());
|
|
1096
|
+
let output = `# Timed-Out Tasks\n\n`;
|
|
1097
|
+
output += `Found ${timedOut.length} task(s) with expired locks.\n\n`;
|
|
1098
|
+
if (timedOut.length === 0) {
|
|
1099
|
+
output += `No timed-out tasks detected. All in-progress tasks have valid locks.\n`;
|
|
1100
|
+
}
|
|
1101
|
+
else {
|
|
1102
|
+
for (const { task, lockExpiredAt, timeSinceExpiry } of timedOut) {
|
|
1103
|
+
const minutesAgo = Math.floor(timeSinceExpiry / 1000 / 60);
|
|
1104
|
+
output += `## ${task.title}\n`;
|
|
1105
|
+
output += `- **Task ID:** \`${task.id}\`\n`;
|
|
1106
|
+
output += `- **Status:** ${task.status}\n`;
|
|
1107
|
+
output += `- **Assigned to:** ${task.assigned_to || 'None'}\n`;
|
|
1108
|
+
output += `- **Lock expired:** ${lockExpiredAt} (${minutesAgo} minutes ago)\n`;
|
|
1109
|
+
output += `- **Parent:** ${task.parent_id || 'None'}\n\n`;
|
|
1110
|
+
}
|
|
1111
|
+
output += `---\n\n`;
|
|
1112
|
+
output += `**Next Steps:**\n`;
|
|
1113
|
+
output += `- Use \`task_retry\` to retry individual tasks\n`;
|
|
1114
|
+
output += `- Use \`task_auto_retry_timeouts\` to automatically handle all timed-out tasks\n`;
|
|
1115
|
+
}
|
|
1116
|
+
return {
|
|
1117
|
+
content: [{ type: 'text', text: output }],
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
case 'task_retry': {
|
|
1121
|
+
const taskId = args?.task_id;
|
|
1122
|
+
const maxRetries = args?.max_retries || 3;
|
|
1123
|
+
const errorContext = args?.error_context;
|
|
1124
|
+
const result = await retryTask(DEFAULT_USER_ID, projectId, taskId, maxRetries, errorContext);
|
|
1125
|
+
mcpLogger.toolResult(name, result.success, timer());
|
|
1126
|
+
let output = '';
|
|
1127
|
+
if (result.success && result.data) {
|
|
1128
|
+
const { task, retryCount, retriesRemaining } = result.data;
|
|
1129
|
+
output = `# Task Retry Successful\n\n`;
|
|
1130
|
+
output += `**Task:** ${task.title}\n`;
|
|
1131
|
+
output += `**Task ID:** \`${taskId}\`\n`;
|
|
1132
|
+
output += `**New Status:** ${task.status} (reset to ready)\n`;
|
|
1133
|
+
output += `**Retry Count:** ${retryCount}/${maxRetries}\n`;
|
|
1134
|
+
output += `**Retries Remaining:** ${retriesRemaining}\n\n`;
|
|
1135
|
+
output += `The task has been reset and is ready to be claimed again.\n\n`;
|
|
1136
|
+
if (errorContext) {
|
|
1137
|
+
output += `**Previous Failure:** ${errorContext}\n`;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
else {
|
|
1141
|
+
output = `# Task Retry Failed\n\n`;
|
|
1142
|
+
output += `**Task ID:** \`${taskId}\`\n`;
|
|
1143
|
+
output += `**Error:** ${result.error}\n\n`;
|
|
1144
|
+
if (result.error?.includes('Retry limit exceeded')) {
|
|
1145
|
+
output += `The task has been marked as **failed** permanently.\n`;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
return {
|
|
1149
|
+
content: [{ type: 'text', text: output }],
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
case 'task_fail': {
|
|
1153
|
+
const taskId = args?.task_id;
|
|
1154
|
+
const errorContext = args?.error_context;
|
|
1155
|
+
const agentId = args?.agent_id;
|
|
1156
|
+
const result = await failTask(DEFAULT_USER_ID, projectId, taskId, errorContext, agentId);
|
|
1157
|
+
mcpLogger.toolResult(name, result.success, timer());
|
|
1158
|
+
let output = `# Task Marked as Failed\n\n`;
|
|
1159
|
+
output += `**Task:** ${result.data.title}\n`;
|
|
1160
|
+
output += `**Task ID:** \`${taskId}\`\n`;
|
|
1161
|
+
output += `**Status:** failed\n`;
|
|
1162
|
+
output += `**Error:** ${errorContext}\n\n`;
|
|
1163
|
+
output += `The task has been marked as failed and the error context has been logged.\n`;
|
|
1164
|
+
return {
|
|
1165
|
+
content: [{ type: 'text', text: output }],
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
case 'task_auto_retry_timeouts': {
|
|
1169
|
+
const maxRetries = args?.max_retries || 3;
|
|
1170
|
+
const result = await autoRetryTimedOutTasks(DEFAULT_USER_ID, projectId, maxRetries);
|
|
1171
|
+
mcpLogger.toolResult(name, true, timer());
|
|
1172
|
+
let output = `# Auto-Retry Timed-Out Tasks\n\n`;
|
|
1173
|
+
output += `**Found:** ${result.timedOut.length} timed-out task(s)\n`;
|
|
1174
|
+
output += `**Retried:** ${result.retried.length} task(s)\n`;
|
|
1175
|
+
output += `**Failed Permanently:** ${result.failed.length} task(s)\n\n`;
|
|
1176
|
+
if (result.retried.length > 0) {
|
|
1177
|
+
output += `## Successfully Retried\n\n`;
|
|
1178
|
+
for (const task of result.retried) {
|
|
1179
|
+
output += `- **${task.title}** (\`${task.id}\`) - reset to ready\n`;
|
|
1180
|
+
}
|
|
1181
|
+
output += `\n`;
|
|
1182
|
+
}
|
|
1183
|
+
if (result.failed.length > 0) {
|
|
1184
|
+
output += `## Failed Permanently (Retry Limit Exceeded)\n\n`;
|
|
1185
|
+
for (const task of result.failed) {
|
|
1186
|
+
output += `- **${task.title}** (\`${task.id}\`) - marked as failed\n`;
|
|
1187
|
+
}
|
|
1188
|
+
output += `\n`;
|
|
1189
|
+
}
|
|
1190
|
+
if (result.timedOut.length === 0) {
|
|
1191
|
+
output += `No timed-out tasks found. All in-progress tasks have valid locks.\n`;
|
|
1192
|
+
}
|
|
1193
|
+
return {
|
|
1194
|
+
content: [{ type: 'text', text: output }],
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
case 'task_orchestrate': {
|
|
1198
|
+
const epicId = args?.epic_id;
|
|
1199
|
+
const maxParallel = args?.max_parallel || 5;
|
|
1200
|
+
const maxRetries = args?.max_retries || 3;
|
|
1201
|
+
const dryRun = args?.dry_run || false;
|
|
1202
|
+
// Get epic status
|
|
1203
|
+
const epicStatus = await getEpicStatus(DEFAULT_USER_ID, projectId, epicId);
|
|
1204
|
+
mcpLogger.toolResult(name, true, timer());
|
|
1205
|
+
// Get dispatchable tasks
|
|
1206
|
+
const dispatchable = await getDispatchableTasks(DEFAULT_USER_ID, projectId, epicId, maxParallel);
|
|
1207
|
+
// Get active task count
|
|
1208
|
+
const activeCount = await getActiveTaskCount(DEFAULT_USER_ID, projectId, epicId);
|
|
1209
|
+
// Get timed-out tasks
|
|
1210
|
+
const timedOut = await detectTimedOutTasks(DEFAULT_USER_ID, projectId);
|
|
1211
|
+
const epicTimedOut = timedOut.filter(t => t.task.parent_id === epicId);
|
|
1212
|
+
// Get blocked tasks
|
|
1213
|
+
const blockedResult = await listTasks(DEFAULT_USER_ID, projectId, {
|
|
1214
|
+
status: 'blocked',
|
|
1215
|
+
parent_id: epicId,
|
|
1216
|
+
limit: 50,
|
|
1217
|
+
});
|
|
1218
|
+
const blockedTasks = blockedResult.success ? blockedResult.data : [];
|
|
1219
|
+
// Get failed tasks
|
|
1220
|
+
const failedResult = await listTasks(DEFAULT_USER_ID, projectId, {
|
|
1221
|
+
status: 'failed',
|
|
1222
|
+
parent_id: epicId,
|
|
1223
|
+
limit: 50,
|
|
1224
|
+
});
|
|
1225
|
+
const failedTasks = failedResult.success ? failedResult.data : [];
|
|
1226
|
+
// Determine overall status
|
|
1227
|
+
let status;
|
|
1228
|
+
if (epicStatus.allComplete) {
|
|
1229
|
+
status = 'complete';
|
|
1230
|
+
}
|
|
1231
|
+
else if (failedTasks.length > 0 && dispatchable.length === 0 && activeCount.total === 0) {
|
|
1232
|
+
status = 'failed';
|
|
1233
|
+
}
|
|
1234
|
+
else if (dispatchable.length === 0 && activeCount.total === 0 && blockedTasks.length > 0) {
|
|
1235
|
+
status = 'blocked';
|
|
1236
|
+
}
|
|
1237
|
+
else if (activeCount.total > 0 || dispatchable.length > 0) {
|
|
1238
|
+
status = 'in_progress';
|
|
1239
|
+
}
|
|
1240
|
+
else {
|
|
1241
|
+
status = 'ready';
|
|
1242
|
+
}
|
|
1243
|
+
// Build response
|
|
1244
|
+
let output = `# Epic Orchestration Status\n\n`;
|
|
1245
|
+
output += `**Epic:** ${epicStatus.epic.title}\n`;
|
|
1246
|
+
output += `**Epic ID:** \`${epicId}\`\n`;
|
|
1247
|
+
output += `**Status:** ${status}\n`;
|
|
1248
|
+
output += `**Progress:** ${epicStatus.percentComplete}% (${epicStatus.byStatus.done || 0}/${epicStatus.totalTasks} tasks)\n`;
|
|
1249
|
+
output += `**Dry Run:** ${dryRun ? 'Yes (preview only)' : 'No'}\n\n`;
|
|
1250
|
+
// Task breakdown
|
|
1251
|
+
output += `## Task Breakdown\n\n`;
|
|
1252
|
+
output += `- ✅ **Done:** ${epicStatus.byStatus.done || 0}\n`;
|
|
1253
|
+
output += `- 🔄 **In Progress:** ${epicStatus.byStatus.in_progress || 0} (${activeCount.claimed} actively claimed)\n`;
|
|
1254
|
+
output += `- 🟢 **Ready/Dispatchable:** ${dispatchable.length}\n`;
|
|
1255
|
+
output += `- 🚫 **Blocked:** ${blockedTasks.length}\n`;
|
|
1256
|
+
output += `- ⏱️ **Timed Out:** ${epicTimedOut.length}\n`;
|
|
1257
|
+
output += `- ❌ **Failed:** ${failedTasks.length}\n`;
|
|
1258
|
+
output += `- 📋 **Backlog:** ${epicStatus.byStatus.backlog || 0}\n\n`;
|
|
1259
|
+
// Dispatchable tasks
|
|
1260
|
+
if (dispatchable.length > 0) {
|
|
1261
|
+
output += `## Dispatchable Tasks (${dispatchable.length})\n\n`;
|
|
1262
|
+
for (const item of dispatchable) {
|
|
1263
|
+
output += `- **${item.task.title}** (\`${item.task.id}\`)\n`;
|
|
1264
|
+
output += ` - Worker: ${item.worker.name}\n`;
|
|
1265
|
+
output += ` - Effort: ${item.task.estimated_effort || 'unknown'}\n`;
|
|
1266
|
+
}
|
|
1267
|
+
output += `\n`;
|
|
1268
|
+
}
|
|
1269
|
+
// In-progress tasks
|
|
1270
|
+
if (activeCount.total > 0) {
|
|
1271
|
+
const inProgressResult = await listTasks(DEFAULT_USER_ID, projectId, {
|
|
1272
|
+
status: 'in_progress',
|
|
1273
|
+
parent_id: epicId,
|
|
1274
|
+
limit: 50,
|
|
1275
|
+
});
|
|
1276
|
+
if (inProgressResult.success && inProgressResult.data.length > 0) {
|
|
1277
|
+
output += `## In Progress (${inProgressResult.data.length})\n\n`;
|
|
1278
|
+
for (const task of inProgressResult.data) {
|
|
1279
|
+
output += `- **${task.title}** (\`${task.id}\`)\n`;
|
|
1280
|
+
output += ` - Assigned: ${task.assigned_to || 'None'}\n`;
|
|
1281
|
+
if (task.lock_expires_at) {
|
|
1282
|
+
const expiresAt = new Date(task.lock_expires_at);
|
|
1283
|
+
const now = new Date();
|
|
1284
|
+
const isExpired = expiresAt < now;
|
|
1285
|
+
output += ` - Lock: ${isExpired ? '⚠️ EXPIRED' : '✅ Active'}\n`;
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
output += `\n`;
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
// Timed-out tasks
|
|
1292
|
+
if (epicTimedOut.length > 0) {
|
|
1293
|
+
output += `## Timed Out (${epicTimedOut.length})\n\n`;
|
|
1294
|
+
for (const { task, timeSinceExpiry } of epicTimedOut) {
|
|
1295
|
+
const minutesAgo = Math.floor(timeSinceExpiry / 1000 / 60);
|
|
1296
|
+
output += `- **${task.title}** (\`${task.id}\`) - ${minutesAgo}m ago\n`;
|
|
1297
|
+
}
|
|
1298
|
+
output += `\n`;
|
|
1299
|
+
}
|
|
1300
|
+
// Blocked tasks
|
|
1301
|
+
if (blockedTasks.length > 0) {
|
|
1302
|
+
output += `## Blocked (${blockedTasks.length})\n\n`;
|
|
1303
|
+
for (const task of blockedTasks.slice(0, 5)) {
|
|
1304
|
+
const blockedBy = parseJsonArray(task.blocked_by);
|
|
1305
|
+
output += `- **${task.title}** (\`${task.id}\`)\n`;
|
|
1306
|
+
output += ` - Blocked by: ${blockedBy.join(', ')}\n`;
|
|
1307
|
+
}
|
|
1308
|
+
if (blockedTasks.length > 5) {
|
|
1309
|
+
output += ` ... and ${blockedTasks.length - 5} more\n`;
|
|
1310
|
+
}
|
|
1311
|
+
output += `\n`;
|
|
1312
|
+
}
|
|
1313
|
+
// Failed tasks
|
|
1314
|
+
if (failedTasks.length > 0) {
|
|
1315
|
+
output += `## Failed (${failedTasks.length})\n\n`;
|
|
1316
|
+
for (const task of failedTasks) {
|
|
1317
|
+
output += `- **${task.title}** (\`${task.id}\`)\n`;
|
|
1318
|
+
}
|
|
1319
|
+
output += `\n`;
|
|
1320
|
+
}
|
|
1321
|
+
// Next actions
|
|
1322
|
+
output += `---\n\n`;
|
|
1323
|
+
output += `## Next Actions\n\n`;
|
|
1324
|
+
const nextActions = [];
|
|
1325
|
+
if (status === 'complete') {
|
|
1326
|
+
nextActions.push('🎉 Epic complete! All tasks done.');
|
|
1327
|
+
}
|
|
1328
|
+
else if (status === 'failed') {
|
|
1329
|
+
nextActions.push(`❌ Epic stalled: ${failedTasks.length} failed task(s), no work remaining.`);
|
|
1330
|
+
nextActions.push('Review failed tasks or manually adjust epic status.');
|
|
1331
|
+
}
|
|
1332
|
+
else if (status === 'blocked') {
|
|
1333
|
+
nextActions.push(`🚫 Epic blocked: All remaining tasks are blocked or waiting.`);
|
|
1334
|
+
nextActions.push('Review dependencies or manually unblock tasks.');
|
|
1335
|
+
}
|
|
1336
|
+
else {
|
|
1337
|
+
// In progress - provide actionable next steps
|
|
1338
|
+
if (epicTimedOut.length > 0) {
|
|
1339
|
+
nextActions.push(`⏱️ Retry ${epicTimedOut.length} timed-out task(s) using task_auto_retry_timeouts`);
|
|
1340
|
+
}
|
|
1341
|
+
if (dispatchable.length > 0) {
|
|
1342
|
+
const canDispatch = Math.min(dispatchable.length, maxParallel - activeCount.total);
|
|
1343
|
+
if (canDispatch > 0) {
|
|
1344
|
+
nextActions.push(`🚀 Spawn ${canDispatch} worker(s) for dispatchable tasks using Task tool`);
|
|
1345
|
+
if (!dryRun) {
|
|
1346
|
+
// Provide Task tool invocations
|
|
1347
|
+
output += `\n**Spawn Commands:**\n\`\`\`\n`;
|
|
1348
|
+
for (const item of dispatchable.slice(0, canDispatch)) {
|
|
1349
|
+
const taskPrompt = `Execute task ${item.task.id}: ${item.task.title}. When complete, call task_complete(task_id="${item.task.id}", summary="...") with work summary.`;
|
|
1350
|
+
output += `Task(subagent_type="general-purpose", prompt="${taskPrompt.replace(/"/g, '\\"')}")\n`;
|
|
1351
|
+
}
|
|
1352
|
+
output += `\`\`\`\n\n`;
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
else {
|
|
1356
|
+
nextActions.push(`⏳ At capacity (${activeCount.total}/${maxParallel}). Wait for tasks to complete.`);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
if (dispatchable.length === 0 && activeCount.total > 0) {
|
|
1360
|
+
nextActions.push(`⏳ Monitor ${activeCount.total} in-progress task(s). No new work to dispatch.`);
|
|
1361
|
+
}
|
|
1362
|
+
if (dispatchable.length === 0 && activeCount.total === 0 && blockedTasks.length === 0 && failedTasks.length === 0) {
|
|
1363
|
+
nextActions.push('✅ Check epic status - may be complete or need refresh.');
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
for (const action of nextActions) {
|
|
1367
|
+
output += `- ${action}\n`;
|
|
1368
|
+
}
|
|
1369
|
+
if (dryRun) {
|
|
1370
|
+
output += `\n**Note:** This is a dry run. No actions were taken.\n`;
|
|
1371
|
+
}
|
|
1372
|
+
return {
|
|
1373
|
+
content: [{ type: 'text', text: output }],
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
930
1376
|
// =========================================================================
|
|
931
1377
|
// CODEBASE MAPPING HANDLERS
|
|
932
1378
|
// =========================================================================
|
|
933
1379
|
case 'codebase_map': {
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
1380
|
+
// Use raw fetch since this endpoint returns markdown, not JSON
|
|
1381
|
+
const url = `${API_BASE_URL}/api/v1/codebase/${projectId}/map`;
|
|
1382
|
+
const response = await fetch(url);
|
|
1383
|
+
mcpLogger.toolResult(name, response.ok, timer());
|
|
1384
|
+
if (!response.ok) {
|
|
937
1385
|
return {
|
|
938
1386
|
content: [{
|
|
939
1387
|
type: 'text',
|
|
@@ -942,8 +1390,9 @@ export function registerToolHandlers(server) {
|
|
|
942
1390
|
};
|
|
943
1391
|
}
|
|
944
1392
|
// The API returns text/markdown directly
|
|
1393
|
+
const markdown = await response.text();
|
|
945
1394
|
return {
|
|
946
|
-
content: [{ type: 'text', text:
|
|
1395
|
+
content: [{ type: 'text', text: markdown }],
|
|
947
1396
|
};
|
|
948
1397
|
}
|
|
949
1398
|
case 'codebase_find': {
|
|
@@ -1085,6 +1534,136 @@ export function registerToolHandlers(server) {
|
|
|
1085
1534
|
content: [{ type: 'text', text: output }],
|
|
1086
1535
|
};
|
|
1087
1536
|
}
|
|
1537
|
+
// =========================================================================
|
|
1538
|
+
// DOCUMENTATION CACHING HANDLERS
|
|
1539
|
+
// =========================================================================
|
|
1540
|
+
case 'docs_cache': {
|
|
1541
|
+
const libraryName = args?.library_name || args?.library;
|
|
1542
|
+
const topic = args?.topic;
|
|
1543
|
+
const result = await cacheDocs(projectId, libraryName, libraryName, undefined, topic);
|
|
1544
|
+
mcpLogger.toolResult(name, true, timer());
|
|
1545
|
+
if (!result.success) {
|
|
1546
|
+
return {
|
|
1547
|
+
content: [{
|
|
1548
|
+
type: 'text',
|
|
1549
|
+
text: `# Failed to Cache Documentation\n\nCould not fetch documentation for "${libraryName}" from Context7.\n\nCheck if the library name is correct (e.g., "react", "next.js", "zod").`,
|
|
1550
|
+
}],
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
let output = `# Documentation Cached\n\n`;
|
|
1554
|
+
output += `**Library:** ${libraryName}\n`;
|
|
1555
|
+
output += `**Status:** ${result.status || 'fetched'}\n`;
|
|
1556
|
+
if (result.size)
|
|
1557
|
+
output += `**Size:** ${result.size} bytes\n`;
|
|
1558
|
+
if (topic)
|
|
1559
|
+
output += `**Topic:** ${topic}\n`;
|
|
1560
|
+
output += `\nDocumentation is now cached from Context7 and available for task context injection.`;
|
|
1561
|
+
return {
|
|
1562
|
+
content: [{ type: 'text', text: output }],
|
|
1563
|
+
};
|
|
1564
|
+
}
|
|
1565
|
+
case 'docs_list': {
|
|
1566
|
+
const result = await listCachedDocs(projectId);
|
|
1567
|
+
mcpLogger.toolResult(name, true, timer());
|
|
1568
|
+
if (result.libraries.length === 0) {
|
|
1569
|
+
return {
|
|
1570
|
+
content: [{
|
|
1571
|
+
type: 'text',
|
|
1572
|
+
text: `# No Cached Documentation\n\nNo documentation has been cached for this project yet.\n\nUse context7 to fetch docs, then \`docs_cache\` to store them.`,
|
|
1573
|
+
}],
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1576
|
+
let output = `# Cached Documentation\n\n`;
|
|
1577
|
+
output += `**Libraries:** ${result.libraries.length}\n\n`;
|
|
1578
|
+
output += `| Library | ID | Topics | Cached |\n`;
|
|
1579
|
+
output += `|---------|----|---------|---------|\n`;
|
|
1580
|
+
for (const lib of result.libraries) {
|
|
1581
|
+
const topics = lib.topics?.join(', ') || 'general';
|
|
1582
|
+
const cached = new Date(lib.cached_at).toLocaleDateString();
|
|
1583
|
+
output += `| ${lib.library_name} | \`${lib.library_id}\` | ${topics} | ${cached} |\n`;
|
|
1584
|
+
}
|
|
1585
|
+
return {
|
|
1586
|
+
content: [{ type: 'text', text: output }],
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
case 'docs_get': {
|
|
1590
|
+
const library = args?.library;
|
|
1591
|
+
const topic = args?.topic;
|
|
1592
|
+
const result = await getCachedDocs(projectId, library, topic);
|
|
1593
|
+
mcpLogger.toolResult(name, true, timer());
|
|
1594
|
+
if (!result || !result.content) {
|
|
1595
|
+
return {
|
|
1596
|
+
content: [{
|
|
1597
|
+
type: 'text',
|
|
1598
|
+
text: `# Documentation Not Found\n\nNo cached documentation found for "${library}"${topic ? ` (topic: ${topic})` : ''}.\n\nUse \`docs_cache\` to fetch and cache the docs first.`,
|
|
1599
|
+
}],
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
let output = `# Documentation: ${result.library_name || result.library || library}\n\n`;
|
|
1603
|
+
output += `**Context7 ID:** ${result.context7Id || 'N/A'}\n`;
|
|
1604
|
+
output += `**Fetched:** ${result.fetchedAt}\n`;
|
|
1605
|
+
output += `**Size:** ${result.size} bytes\n`;
|
|
1606
|
+
if (topic)
|
|
1607
|
+
output += `**Topic:** ${topic}\n`;
|
|
1608
|
+
output += `\n---\n\n`;
|
|
1609
|
+
output += result.content;
|
|
1610
|
+
return {
|
|
1611
|
+
content: [{ type: 'text', text: output }],
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1614
|
+
// =========================================================================
|
|
1615
|
+
// CODEDNA CODE GENERATION HANDLERS
|
|
1616
|
+
// =========================================================================
|
|
1617
|
+
case 'codedna_generate_api': {
|
|
1618
|
+
const result = await handleGenerateApi(args);
|
|
1619
|
+
mcpLogger.toolResult(name, true, timer());
|
|
1620
|
+
return {
|
|
1621
|
+
content: [{
|
|
1622
|
+
type: 'text',
|
|
1623
|
+
text: JSON.stringify(result, null, 2),
|
|
1624
|
+
}],
|
|
1625
|
+
};
|
|
1626
|
+
}
|
|
1627
|
+
case 'codedna_generate_frontend': {
|
|
1628
|
+
const result = await handleGenerateFrontend(args);
|
|
1629
|
+
mcpLogger.toolResult(name, true, timer());
|
|
1630
|
+
return {
|
|
1631
|
+
content: [{
|
|
1632
|
+
type: 'text',
|
|
1633
|
+
text: JSON.stringify(result, null, 2),
|
|
1634
|
+
}],
|
|
1635
|
+
};
|
|
1636
|
+
}
|
|
1637
|
+
case 'codedna_generate_component': {
|
|
1638
|
+
const result = await handleGenerateComponent(args);
|
|
1639
|
+
mcpLogger.toolResult(name, true, timer());
|
|
1640
|
+
return {
|
|
1641
|
+
content: [{
|
|
1642
|
+
type: 'text',
|
|
1643
|
+
text: JSON.stringify(result, null, 2),
|
|
1644
|
+
}],
|
|
1645
|
+
};
|
|
1646
|
+
}
|
|
1647
|
+
case 'codedna_list_generators': {
|
|
1648
|
+
const result = await handleListGenerators();
|
|
1649
|
+
mcpLogger.toolResult(name, true, timer());
|
|
1650
|
+
return {
|
|
1651
|
+
content: [{
|
|
1652
|
+
type: 'text',
|
|
1653
|
+
text: JSON.stringify(result, null, 2),
|
|
1654
|
+
}],
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1657
|
+
case 'codedna_validate_spec': {
|
|
1658
|
+
const result = await handleValidateSpec(args);
|
|
1659
|
+
mcpLogger.toolResult(name, true, timer());
|
|
1660
|
+
return {
|
|
1661
|
+
content: [{
|
|
1662
|
+
type: 'text',
|
|
1663
|
+
text: JSON.stringify(result, null, 2),
|
|
1664
|
+
}],
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1088
1667
|
default:
|
|
1089
1668
|
throw new Error(`Unknown tool: ${name}`);
|
|
1090
1669
|
}
|