feat(research): Add comprehensive AI-powered research command with interactive follow-ups, save functionality, intelligent context gathering, fuzzy task discovery, multi-source context support, enhanced display with syntax highlighting, clean inquirer menus, comprehensive help, and MCP integration with saveTo parameter

This commit is contained in:
Eyal Toledano
2025-06-13 04:22:31 -04:00
parent 3c8c62434f
commit 932825c2d6
9 changed files with 7369 additions and 6879 deletions

View File

@@ -44,5 +44,34 @@ task-master research "Quick implementation steps?" --context="Using JWT tokens"
- Fresh fuzzy search for each follow-up to discover newly relevant tasks
- Cumulative context building across the conversation
- Clean visual separation between exchanges
- **Save to Tasks**: Save entire research conversations (including follow-ups) directly to task or subtask details with timestamps
- **Clean Menu Interface**: Streamlined inquirer-based menu for follow-up actions without redundant UI elements
The research command integrates with the existing AI service layer and supports all configured AI providers. MCP integration provides the same functionality for programmatic access without interactive features.
**Save Functionality:**
The research command now supports saving complete conversation threads to tasks or subtasks:
- Save research results and follow-up conversations to any task (e.g., "15") or subtask (e.g., "15.2")
- Automatic timestamping and formatting of conversation history
- Validation of task/subtask existence before saving
- Appends to existing task details without overwriting content
- Supports both CLI interactive mode and MCP programmatic access via `--save-to` flag
**Enhanced CLI Options:**
```bash
# Auto-save research results to a task
task-master research "Implementation approach?" --save-to=15
# Combine auto-save with context gathering
task-master research "How to optimize this?" --id=23 --save-to=23.1
```
**MCP Integration:**
- `saveTo` parameter for automatic saving to specified task/subtask ID
- Structured response format with telemetry data
- Silent operation mode for programmatic usage
- Full feature parity with CLI except interactive follow-ups
The research command integrates with the existing AI service layer and supports all configured AI providers. Both CLI and MCP interfaces provide comprehensive research capabilities with intelligent context gathering and flexible output options.

View File

@@ -1,6 +1,6 @@
{
"currentTag": "test-prd-tag",
"lastSwitched": "2025-06-13T07:48:41.146Z",
"currentTag": "master",
"lastSwitched": "2025-06-13T07:55:31.313Z",
"branchTagMapping": {},
"migrationNoticeShown": true
}

File diff suppressed because one or more lines are too long

View File

@@ -3,6 +3,7 @@
* Direct function implementation for AI-powered research queries
*/
import path from 'path';
import { performResearch } from '../../../../scripts/modules/task-manager.js';
import {
enableSilentMode,
@@ -20,6 +21,7 @@ import { createLogWrapper } from '../../tools/utils.js';
* @param {string} [args.customContext] - Additional custom context text
* @param {boolean} [args.includeProjectTree=false] - Include project file tree in context
* @param {string} [args.detailLevel='medium'] - Detail level: 'low', 'medium', 'high'
* @param {string} [args.saveTo] - Automatically save to task/subtask ID (e.g., "15" or "15.2")
* @param {string} [args.projectRoot] - Project root path
* @param {Object} log - Logger object
* @param {Object} context - Additional context (session)
@@ -34,6 +36,7 @@ export async function researchDirect(args, log, context = {}) {
customContext,
includeProjectTree = false,
detailLevel = 'medium',
saveTo,
projectRoot
} = args;
const { session } = context; // Destructure session from context
@@ -125,6 +128,88 @@ export async function researchDirect(args, log, context = {}) {
false // allowFollowUp - disable for MCP calls
);
// Auto-save to task/subtask if requested
if (saveTo) {
try {
const isSubtask = saveTo.includes('.');
// Format research content for saving
const researchContent = `## Research Query: ${query.trim()}
**Detail Level:** ${result.detailLevel}
**Context Size:** ${result.contextSize} characters
**Timestamp:** ${new Date().toLocaleDateString()} ${new Date().toLocaleTimeString()}
### Results
${result.result}`;
if (isSubtask) {
// Save to subtask
const { updateSubtaskById } = await import(
'../../../../scripts/modules/task-manager/update-subtask-by-id.js'
);
const tasksPath = path.join(
projectRoot,
'.taskmaster',
'tasks',
'tasks.json'
);
await updateSubtaskById(
tasksPath,
saveTo,
researchContent,
false, // useResearch = false for simple append
{
session,
mcpLog,
commandName: 'research-save',
outputType: 'mcp',
projectRoot
},
'json'
);
log.info(`Research saved to subtask ${saveTo}`);
} else {
// Save to task
const updateTaskById = (
await import(
'../../../../scripts/modules/task-manager/update-task-by-id.js'
)
).default;
const taskIdNum = parseInt(saveTo, 10);
const tasksPath = path.join(
projectRoot,
'.taskmaster',
'tasks',
'tasks.json'
);
await updateTaskById(
tasksPath,
taskIdNum,
researchContent,
false, // useResearch = false for simple append
{
session,
mcpLog,
commandName: 'research-save',
outputType: 'mcp',
projectRoot
},
'json',
true // appendMode = true
);
log.info(`Research saved to task ${saveTo}`);
}
} catch (saveError) {
log.warn(`Error saving research to task/subtask: ${saveError.message}`);
}
}
// Restore normal logging
disableSilentMode();

View File

@@ -47,6 +47,12 @@ export function registerResearchTool(server) {
.enum(['low', 'medium', 'high'])
.optional()
.describe('Detail level for the research response (default: medium)'),
saveTo: z
.string()
.optional()
.describe(
'Automatically save research results to specified task/subtask ID (e.g., "15" or "15.2")'
),
projectRoot: z
.string()
.describe('The directory of the project. Must be an absolute path.')
@@ -66,6 +72,7 @@ export function registerResearchTool(server) {
customContext: args.customContext,
includeProjectTree: args.includeProjectTree || false,
detailLevel: args.detailLevel || 'medium',
saveTo: args.saveTo,
projectRoot: args.projectRoot
},
log,

View File

@@ -1607,7 +1607,7 @@ function registerCommands(programInstance) {
programInstance
.command('research')
.description('Perform AI-powered research queries with project context')
.argument('<prompt>', 'Research prompt to investigate')
.argument('[prompt]', 'Research prompt to investigate')
.option('--file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.option(
'-i, --id <ids>',
@@ -1634,6 +1634,10 @@ function registerCommands(programInstance) {
'Output detail level: low, medium, high',
'medium'
)
.option(
'--save-to <id>',
'Automatically save research results to specified task/subtask ID (e.g., "15" or "15.2")'
)
.option('--tag <tag>', 'Specify tag context for task operations')
.action(async (prompt, options) => {
// Parameter validation
@@ -1641,6 +1645,7 @@ function registerCommands(programInstance) {
console.error(
chalk.red('Error: Research prompt is required and cannot be empty')
);
showResearchHelp();
process.exit(1);
}
@@ -1697,7 +1702,25 @@ function registerCommands(programInstance) {
}
}
// Validate save option if provided
// Validate save-to option if provided
if (options.saveTo) {
const saveToId = options.saveTo.trim();
if (saveToId.length === 0) {
console.error(chalk.red('Error: Save-to ID cannot be empty'));
process.exit(1);
}
// Validate ID format: number or number.number
if (!/^\d+(\.\d+)?$/.test(saveToId)) {
console.error(
chalk.red(
'Error: Save-to ID must be in format "15" for task or "15.2" for subtask'
)
);
process.exit(1);
}
}
// Validate save option if provided (legacy file save)
if (options.save) {
const saveTarget = options.save.trim();
if (saveTarget.length === 0) {
@@ -1765,6 +1788,8 @@ function registerCommands(programInstance) {
customContext: options.context ? options.context.trim() : null,
includeProjectTree: !!options.tree,
saveTarget: options.save ? options.save.trim() : null,
saveToId: options.saveTo ? options.saveTo.trim() : null,
allowFollowUp: true, // Always allow follow-up in CLI
detailLevel: options.detail ? options.detail.toLowerCase() : 'medium',
tasksPath: tasksPath,
projectRoot: projectRoot
@@ -1822,10 +1847,85 @@ function registerCommands(programInstance) {
commandName: 'research',
outputType: 'cli'
},
'text',
validatedParams.allowFollowUp // Pass follow-up flag
);
// Auto-save to task/subtask if requested
if (validatedParams.saveToId) {
try {
const isSubtask = validatedParams.saveToId.includes('.');
// Format research content for saving
const researchContent = `## Research Query: ${validatedParams.prompt}
**Detail Level:** ${result.detailLevel}
**Context Size:** ${result.contextSize} characters
**Timestamp:** ${new Date().toLocaleDateString()} ${new Date().toLocaleTimeString()}
### Results
${result.result}`;
if (isSubtask) {
// Save to subtask
const { updateSubtaskById } = await import(
'./task-manager/update-subtask-by-id.js'
);
await updateSubtaskById(
validatedParams.tasksPath,
validatedParams.saveToId,
researchContent,
false, // useResearch = false for simple append
{
commandName: 'research-save',
outputType: 'cli',
projectRoot: validatedParams.projectRoot
},
'text'
);
// Save results if requested
console.log(
chalk.green(
`✅ Research saved to subtask ${validatedParams.saveToId}`
)
);
} else {
// Save to task
const updateTaskById = (
await import('./task-manager/update-task-by-id.js')
).default;
const taskIdNum = parseInt(validatedParams.saveToId, 10);
await updateTaskById(
validatedParams.tasksPath,
taskIdNum,
researchContent,
false, // useResearch = false for simple append
{
commandName: 'research-save',
outputType: 'cli',
projectRoot: validatedParams.projectRoot
},
'text',
true // appendMode = true
);
console.log(
chalk.green(
`✅ Research saved to task ${validatedParams.saveToId}`
)
);
}
} catch (saveError) {
console.log(
chalk.red(`❌ Error saving to task/subtask: ${saveError.message}`)
);
}
}
// Save results to file if requested (legacy)
if (validatedParams.saveTarget) {
const saveContent = `# Research Query: ${validatedParams.prompt}
@@ -2801,6 +2901,40 @@ ${result.result}
);
}
// Helper function to show research command help
function showResearchHelp() {
console.log(
boxen(
chalk.white.bold('Research Command Help') +
'\n\n' +
chalk.cyan('Usage:') +
'\n' +
` task-master research "<query>" [options]\n\n` +
chalk.cyan('Required:') +
'\n' +
' <query> Research question or prompt (required)\n\n' +
chalk.cyan('Context Options:') +
'\n' +
' -i, --id <ids> Comma-separated task/subtask IDs for context (e.g., "15,23.2")\n' +
' -f, --files <paths> Comma-separated file paths for context\n' +
' -c, --context <text> Additional custom context text\n' +
' --tree Include project file tree structure\n\n' +
chalk.cyan('Output Options:') +
'\n' +
' -d, --detail <level> Detail level: low, medium, high (default: medium)\n' +
' --save-to <id> Auto-save results to task/subtask ID (e.g., "15" or "15.2")\n' +
' --tag <tag> Specify tag context for task operations\n\n' +
chalk.cyan('Examples:') +
'\n' +
' task-master research "How should I implement user authentication?"\n' +
' task-master research "What\'s the best approach?" --id=15,23.2\n' +
' task-master research "How does auth work?" --files=src/auth.js --tree\n' +
' task-master research "Implementation steps?" --save-to=15.2 --detail=high',
{ padding: 1, borderColor: 'blue', borderStyle: 'round' }
)
);
}
// remove-task command
programInstance
.command('remove-task')

View File

@@ -380,21 +380,6 @@ async function addTask(
displayContextAnalysis(analysisData, prompt, gatheredContext.length);
}
if (outputFormat === 'text') {
console.log(
boxen(
chalk.white.bold('AI Task Generation') +
`\n\n${chalk.gray('Analyzing context and generating task details using AI...')}`,
{
padding: { top: 0, bottom: 1, left: 1, right: 1 },
margin: { top: 1, bottom: 0 },
borderColor: 'white',
borderStyle: 'round'
}
)
);
}
// System Prompt - Enhanced for dependency awareness
const systemPrompt =
"You are a helpful assistant that creates well-structured tasks for a software development project. Generate a single new task based on the user's description, adhering strictly to the provided JSON schema. Pay special attention to dependencies between tasks, ensuring the new task correctly references any tasks it depends on.\n\n" +

View File

@@ -3,6 +3,7 @@
* Core research functionality for AI-powered queries with project context
*/
import fs from 'fs';
import path from 'path';
import chalk from 'chalk';
import boxen from 'boxen';
@@ -587,7 +588,7 @@ function displayResearchResults(result, query, detailLevel, tokenBreakdown) {
}
/**
* Handle follow-up questions in interactive mode
* Handle follow-up questions and save functionality in interactive mode
* @param {Object} originalOptions - Original research options
* @param {Object} context - Execution context
* @param {string} outputFormat - Output format
@@ -606,30 +607,53 @@ async function handleFollowUpQuestions(
initialResult
) {
try {
// Import required modules for saving
const { readJSON } = await import('../utils.js');
const updateTaskById = (await import('./update-task-by-id.js')).default;
const { updateSubtaskById } = await import('./update-subtask-by-id.js');
// Initialize conversation history with the initial Q&A
const conversationHistory = [
{
question: initialQuery,
answer: initialResult,
type: 'initial'
type: 'initial',
timestamp: new Date().toISOString()
}
];
while (true) {
// Ask if user wants to ask a follow-up question
const { wantFollowUp } = await inquirer.prompt([
// Get user choice
const { action } = await inquirer.prompt([
{
type: 'confirm',
name: 'wantFollowUp',
message: 'Would you like to ask a follow-up question?',
default: false // Default to 'n' as requested
type: 'list',
name: 'action',
message: 'What would you like to do next?',
choices: [
{ name: 'Ask a follow-up question', value: 'followup' },
{ name: 'Save to task/subtask', value: 'save' },
{ name: 'Quit', value: 'quit' }
],
pageSize: 3
}
]);
if (!wantFollowUp) {
if (action === 'quit') {
break;
}
if (action === 'save') {
// Handle save functionality
await handleSaveToTask(
conversationHistory,
projectRoot,
context,
logFn
);
continue;
}
if (action === 'followup') {
// Get the follow-up question
const { followUpQuery } = await inquirer.prompt([
{
@@ -652,10 +676,10 @@ async function handleFollowUpQuestions(
console.log('\n' + chalk.gray('─'.repeat(60)) + '\n');
// Build cumulative conversation context from all previous exchanges
const conversationContext = buildConversationContext(conversationHistory);
const conversationContext =
buildConversationContext(conversationHistory);
// Create enhanced options for follow-up with full conversation context
// Remove explicit task IDs to allow fresh fuzzy search based on new question
const followUpOptions = {
...originalOptions,
taskIds: [], // Clear task IDs to allow fresh fuzzy search
@@ -666,8 +690,7 @@ async function handleFollowUpQuestions(
: '')
};
// Perform follow-up research with fresh fuzzy search and conversation context
// Disable follow-up prompts for nested calls to prevent infinite recursion
// Perform follow-up research
const followUpResult = await performResearch(
followUpQuery.trim(),
followUpOptions,
@@ -680,9 +703,11 @@ async function handleFollowUpQuestions(
conversationHistory.push({
question: followUpQuery.trim(),
answer: followUpResult.result,
type: 'followup'
type: 'followup',
timestamp: new Date().toISOString()
});
}
}
} catch (error) {
// If there's an error with inquirer (e.g., non-interactive terminal),
// silently continue without follow-up functionality
@@ -690,6 +715,173 @@ async function handleFollowUpQuestions(
}
}
/**
* Handle saving conversation to a task or subtask
* @param {Array} conversationHistory - Array of conversation exchanges
* @param {string} projectRoot - Project root directory
* @param {Object} context - Execution context
* @param {Object} logFn - Logger function
*/
async function handleSaveToTask(
conversationHistory,
projectRoot,
context,
logFn
) {
try {
// Import required modules
const { readJSON } = await import('../utils.js');
const updateTaskById = (await import('./update-task-by-id.js')).default;
const { updateSubtaskById } = await import('./update-subtask-by-id.js');
// Get task ID from user
const { taskId } = await inquirer.prompt([
{
type: 'input',
name: 'taskId',
message: 'Enter task ID (e.g., "15" for task or "15.2" for subtask):',
validate: (input) => {
if (!input || input.trim().length === 0) {
return 'Please enter a task ID.';
}
const trimmedInput = input.trim();
// Validate format: number or number.number
if (!/^\d+(\.\d+)?$/.test(trimmedInput)) {
return 'Invalid format. Use "15" for task or "15.2" for subtask.';
}
return true;
}
}
]);
const trimmedTaskId = taskId.trim();
// Format conversation thread for saving
const conversationThread = formatConversationForSaving(conversationHistory);
// Determine if it's a task or subtask
const isSubtask = trimmedTaskId.includes('.');
// Try to save - first validate the ID exists
const tasksPath = path.join(
projectRoot,
'.taskmaster',
'tasks',
'tasks.json'
);
if (!fs.existsSync(tasksPath)) {
console.log(
chalk.red('❌ Tasks file not found. Please run task-master init first.')
);
return;
}
// Validate ID exists
const data = readJSON(tasksPath, projectRoot);
if (!data || !data.tasks) {
console.log(chalk.red('❌ No valid tasks found.'));
return;
}
if (isSubtask) {
// Validate subtask exists
const [parentId, subtaskId] = trimmedTaskId
.split('.')
.map((id) => parseInt(id, 10));
const parentTask = data.tasks.find((t) => t.id === parentId);
if (!parentTask) {
console.log(chalk.red(`❌ Parent task ${parentId} not found.`));
return;
}
if (
!parentTask.subtasks ||
!parentTask.subtasks.find((st) => st.id === subtaskId)
) {
console.log(chalk.red(`❌ Subtask ${trimmedTaskId} not found.`));
return;
}
// Save to subtask using updateSubtaskById
console.log(chalk.blue('💾 Saving research conversation to subtask...'));
await updateSubtaskById(
tasksPath,
trimmedTaskId,
conversationThread,
false, // useResearch = false for simple append
context,
'text'
);
console.log(
chalk.green(
`✅ Research conversation saved to subtask ${trimmedTaskId}`
)
);
} else {
// Validate task exists
const taskIdNum = parseInt(trimmedTaskId, 10);
const task = data.tasks.find((t) => t.id === taskIdNum);
if (!task) {
console.log(chalk.red(`❌ Task ${trimmedTaskId} not found.`));
return;
}
// Save to task using updateTaskById with append mode
console.log(chalk.blue('💾 Saving research conversation to task...'));
await updateTaskById(
tasksPath,
taskIdNum,
conversationThread,
false, // useResearch = false for simple append
context,
'text',
true // appendMode = true
);
console.log(
chalk.green(`✅ Research conversation saved to task ${trimmedTaskId}`)
);
}
} catch (error) {
console.log(chalk.red(`❌ Error saving conversation: ${error.message}`));
logFn.error(`Error saving conversation: ${error.message}`);
}
}
/**
* Format conversation history for saving to a task/subtask
* @param {Array} conversationHistory - Array of conversation exchanges
* @returns {string} Formatted conversation thread
*/
function formatConversationForSaving(conversationHistory) {
const timestamp = new Date().toISOString();
let formatted = `## Research Session - ${new Date().toLocaleDateString()} ${new Date().toLocaleTimeString()}\n\n`;
conversationHistory.forEach((exchange, index) => {
if (exchange.type === 'initial') {
formatted += `**Initial Query:** ${exchange.question}\n\n`;
formatted += `**Response:** ${exchange.answer}\n\n`;
} else {
formatted += `**Follow-up ${index}:** ${exchange.question}\n\n`;
formatted += `**Response:** ${exchange.answer}\n\n`;
}
if (index < conversationHistory.length - 1) {
formatted += '---\n\n';
}
});
return formatted;
}
/**
* Build conversation context string from conversation history
* @param {Array} conversationHistory - Array of conversation exchanges

View File

@@ -201,16 +201,18 @@ function parseUpdatedTaskFromText(text, expectedTaskId, logFn, isMCP) {
}
/**
* Update a single task by ID using the unified AI service.
* Update a task by ID with new information using the unified AI service.
* @param {string} tasksPath - Path to the tasks.json file
* @param {number} taskId - Task ID to update
* @param {string} prompt - Prompt with new context
* @param {number} taskId - ID of the task to update
* @param {string} prompt - Prompt for generating updated task information
* @param {boolean} [useResearch=false] - Whether to use the research AI role.
* @param {Object} context - Context object containing session and mcpLog.
* @param {Object} [context.session] - Session object from MCP server.
* @param {Object} [context.mcpLog] - MCP logger object.
* @param {string} [context.projectRoot] - Project root path.
* @param {string} [outputFormat='text'] - Output format ('text' or 'json').
* @returns {Promise<Object|null>} - Updated task data or null if task wasn't updated/found.
* @param {boolean} [appendMode=false] - If true, append to details instead of full update.
* @returns {Promise<Object|null>} - The updated task or null if update failed.
*/
async function updateTaskById(
tasksPath,
@@ -218,7 +220,8 @@ async function updateTaskById(
prompt,
useResearch = false,
context = {},
outputFormat = 'text'
outputFormat = 'text',
appendMode = false
) {
const { session, mcpLog, projectRoot: providedProjectRoot } = context;
const logFn = mcpLog || consoleLog;
@@ -388,8 +391,41 @@ async function updateTaskById(
);
}
// --- Build Prompts (Keep EXACT original prompts) ---
const systemPrompt = `You are an AI assistant helping to update a software development task based on new context.
// --- Build Prompts (Different for append vs full update) ---
let systemPrompt;
let userPrompt;
if (appendMode) {
// Append mode: generate new content to add to task details
systemPrompt = `You are an AI assistant helping to append additional information to a software development task. You will be provided with the task's existing details, context, and a user request string.
Your Goal: Based *only* on the user's request and all the provided context (including existing details if relevant to the request), GENERATE the new text content that should be added to the task's details.
Focus *only* on generating the substance of the update.
Output Requirements:
1. Return *only* the newly generated text content as a plain string. Do NOT return a JSON object or any other structured data.
2. Your string response should NOT include any of the task's original details, unless the user's request explicitly asks to rephrase, summarize, or directly modify existing text.
3. Do NOT include any timestamps, XML-like tags, markdown, or any other special formatting in your string response.
4. Ensure the generated text is concise yet complete for the update based on the user request. Avoid conversational fillers or explanations about what you are doing (e.g., do not start with "Okay, here's the update...").`;
const taskContext = `
Task: ${JSON.stringify({
id: taskToUpdate.id,
title: taskToUpdate.title,
description: taskToUpdate.description,
status: taskToUpdate.status
})}
Current Task Details (for context only):\n${taskToUpdate.details || '(No existing details)'}
`;
userPrompt = `Task Context:\n${taskContext}\n\nUser Request: "${prompt}"\n\nBased on the User Request and all the Task Context (including current task details provided above), what is the new information or text that should be appended to this task's details? Return ONLY this new text as a plain string.`;
if (gatheredContext) {
userPrompt += `\n\n# Additional Project Context\n\n${gatheredContext}`;
}
} else {
// Full update mode: use original prompts
systemPrompt = `You are an AI assistant helping to update a software development task based on new context.
You will be given a task and a prompt describing changes or new implementation details.
Your job is to update the task to reflect these changes, while preserving its basic structure.
@@ -408,14 +444,15 @@ Guidelines:
The changes described in the prompt should be thoughtfully applied to make the task more accurate and actionable.`;
const taskDataString = JSON.stringify(taskToUpdate, null, 2); // Use original task data
let userPrompt = `Here is the task to update:\n${taskDataString}\n\nPlease update this task based on the following new context:\n${prompt}\n\nIMPORTANT: In the task JSON above, any subtasks with "status": "done" or "status": "completed" should be preserved exactly as is. Build your changes around these completed items.`;
const taskDataString = JSON.stringify(taskToUpdate, null, 2);
userPrompt = `Here is the task to update:\n${taskDataString}\n\nPlease update this task based on the following new context:\n${prompt}\n\nIMPORTANT: In the task JSON above, any subtasks with "status": "done" or "status": "completed" should be preserved exactly as is. Build your changes around these completed items.`;
if (gatheredContext) {
userPrompt += `\n\n# Project Context\n\n${gatheredContext}`;
}
userPrompt += `\n\nReturn only the updated task as a valid JSON object.`;
}
// --- End Build Prompts ---
let loadingIndicator = null;
@@ -442,7 +479,72 @@ The changes described in the prompt should be thoughtfully applied to make the t
if (loadingIndicator)
stopLoadingIndicator(loadingIndicator, 'AI update complete.');
// Use mainResult (text) for parsing
if (appendMode) {
// Append mode: handle as plain text
const generatedContentString = aiServiceResponse.mainResult;
let newlyAddedSnippet = '';
if (generatedContentString && generatedContentString.trim()) {
const timestamp = new Date().toISOString();
const formattedBlock = `<info added on ${timestamp}>\n${generatedContentString.trim()}\n</info added on ${timestamp}>`;
newlyAddedSnippet = formattedBlock;
// Append to task details
taskToUpdate.details =
(taskToUpdate.details ? taskToUpdate.details + '\n' : '') +
formattedBlock;
} else {
report(
'warn',
'AI response was empty or whitespace after trimming. Original details remain unchanged.'
);
newlyAddedSnippet = 'No new details were added by the AI.';
}
// Update description with timestamp if prompt is short
if (prompt.length < 100) {
if (taskToUpdate.description) {
taskToUpdate.description += ` [Updated: ${new Date().toLocaleDateString()}]`;
}
}
// Write the updated task back to file
data.tasks[taskIndex] = taskToUpdate;
writeJSON(tasksPath, data);
report('success', `Successfully appended to task ${taskId}`);
// Display success message for CLI
if (outputFormat === 'text') {
console.log(
boxen(
chalk.green(`Successfully appended to task #${taskId}`) +
'\n\n' +
chalk.white.bold('Title:') +
' ' +
taskToUpdate.title +
'\n\n' +
chalk.white.bold('Newly Added Content:') +
'\n' +
chalk.white(newlyAddedSnippet),
{ padding: 1, borderColor: 'green', borderStyle: 'round' }
)
);
}
// Display AI usage telemetry for CLI users
if (outputFormat === 'text' && aiServiceResponse.telemetryData) {
displayAiUsageSummary(aiServiceResponse.telemetryData, 'cli');
}
// Return the updated task
return {
updatedTask: taskToUpdate,
telemetryData: aiServiceResponse.telemetryData,
tagInfo: aiServiceResponse.tagInfo
};
}
// Full update mode: Use mainResult (text) for parsing
const updatedTask = parseUpdatedTaskFromText(
aiServiceResponse.mainResult,
taskId,