@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.
@@ -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
- createdTasks.push({ task: taskResult.data, worker });
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
- let output = `# Work Plan Created\n\n`;
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\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 += `## Execution Options\n\n`;
448
- output += `**Sequential:** Use \`task_start\` with each task ID.\n\n`;
449
- output += `**Parallel:** Use \`task_dispatch\` to spawn workers:\n`;
450
- output += `\`\`\`\ntask_dispatch(epic_id="${epic.id}")\n\`\`\`\n`;
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
- const result = await apiRequest(`/api/v1/codebase/${projectId}/map`);
935
- mcpLogger.toolResult(name, true, timer());
936
- if (!result.success || typeof result === 'object' && 'error' in result) {
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: result }],
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
  }