chore: run npm run format
This commit is contained in:
@@ -2,84 +2,90 @@ import { jest } from '@jest/globals';
|
||||
import { ContextManager } from '../context-manager.js';
|
||||
|
||||
describe('ContextManager', () => {
|
||||
let contextManager;
|
||||
let contextManager;
|
||||
|
||||
beforeEach(() => {
|
||||
contextManager = new ContextManager({
|
||||
maxCacheSize: 10,
|
||||
ttl: 1000, // 1 second for testing
|
||||
maxContextSize: 1000
|
||||
});
|
||||
});
|
||||
beforeEach(() => {
|
||||
contextManager = new ContextManager({
|
||||
maxCacheSize: 10,
|
||||
ttl: 1000, // 1 second for testing
|
||||
maxContextSize: 1000
|
||||
});
|
||||
});
|
||||
|
||||
describe('getContext', () => {
|
||||
it('should create a new context when not in cache', async () => {
|
||||
const context = await contextManager.getContext('test-id', { test: true });
|
||||
expect(context.id).toBe('test-id');
|
||||
expect(context.metadata.test).toBe(true);
|
||||
expect(contextManager.stats.misses).toBe(1);
|
||||
expect(contextManager.stats.hits).toBe(0);
|
||||
});
|
||||
describe('getContext', () => {
|
||||
it('should create a new context when not in cache', async () => {
|
||||
const context = await contextManager.getContext('test-id', {
|
||||
test: true
|
||||
});
|
||||
expect(context.id).toBe('test-id');
|
||||
expect(context.metadata.test).toBe(true);
|
||||
expect(contextManager.stats.misses).toBe(1);
|
||||
expect(contextManager.stats.hits).toBe(0);
|
||||
});
|
||||
|
||||
it('should return cached context when available', async () => {
|
||||
// First call creates the context
|
||||
await contextManager.getContext('test-id', { test: true });
|
||||
|
||||
// Second call should hit cache
|
||||
const context = await contextManager.getContext('test-id', { test: true });
|
||||
expect(context.id).toBe('test-id');
|
||||
expect(context.metadata.test).toBe(true);
|
||||
expect(contextManager.stats.hits).toBe(1);
|
||||
expect(contextManager.stats.misses).toBe(1);
|
||||
});
|
||||
it('should return cached context when available', async () => {
|
||||
// First call creates the context
|
||||
await contextManager.getContext('test-id', { test: true });
|
||||
|
||||
it('should respect TTL settings', async () => {
|
||||
// Create context
|
||||
await contextManager.getContext('test-id', { test: true });
|
||||
|
||||
// Wait for TTL to expire
|
||||
await new Promise(resolve => setTimeout(resolve, 1100));
|
||||
|
||||
// Should create new context
|
||||
await contextManager.getContext('test-id', { test: true });
|
||||
expect(contextManager.stats.misses).toBe(2);
|
||||
expect(contextManager.stats.hits).toBe(0);
|
||||
});
|
||||
});
|
||||
// Second call should hit cache
|
||||
const context = await contextManager.getContext('test-id', {
|
||||
test: true
|
||||
});
|
||||
expect(context.id).toBe('test-id');
|
||||
expect(context.metadata.test).toBe(true);
|
||||
expect(contextManager.stats.hits).toBe(1);
|
||||
expect(contextManager.stats.misses).toBe(1);
|
||||
});
|
||||
|
||||
describe('updateContext', () => {
|
||||
it('should update existing context metadata', async () => {
|
||||
await contextManager.getContext('test-id', { initial: true });
|
||||
const updated = await contextManager.updateContext('test-id', { updated: true });
|
||||
|
||||
expect(updated.metadata.initial).toBe(true);
|
||||
expect(updated.metadata.updated).toBe(true);
|
||||
});
|
||||
});
|
||||
it('should respect TTL settings', async () => {
|
||||
// Create context
|
||||
await contextManager.getContext('test-id', { test: true });
|
||||
|
||||
describe('invalidateContext', () => {
|
||||
it('should remove context from cache', async () => {
|
||||
await contextManager.getContext('test-id', { test: true });
|
||||
contextManager.invalidateContext('test-id', { test: true });
|
||||
|
||||
// Should be a cache miss
|
||||
await contextManager.getContext('test-id', { test: true });
|
||||
expect(contextManager.stats.invalidations).toBe(1);
|
||||
expect(contextManager.stats.misses).toBe(2);
|
||||
});
|
||||
});
|
||||
// Wait for TTL to expire
|
||||
await new Promise((resolve) => setTimeout(resolve, 1100));
|
||||
|
||||
describe('getStats', () => {
|
||||
it('should return current cache statistics', async () => {
|
||||
await contextManager.getContext('test-id', { test: true });
|
||||
const stats = contextManager.getStats();
|
||||
|
||||
expect(stats.hits).toBe(0);
|
||||
expect(stats.misses).toBe(1);
|
||||
expect(stats.invalidations).toBe(0);
|
||||
expect(stats.size).toBe(1);
|
||||
expect(stats.maxSize).toBe(10);
|
||||
expect(stats.ttl).toBe(1000);
|
||||
});
|
||||
});
|
||||
});
|
||||
// Should create new context
|
||||
await contextManager.getContext('test-id', { test: true });
|
||||
expect(contextManager.stats.misses).toBe(2);
|
||||
expect(contextManager.stats.hits).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateContext', () => {
|
||||
it('should update existing context metadata', async () => {
|
||||
await contextManager.getContext('test-id', { initial: true });
|
||||
const updated = await contextManager.updateContext('test-id', {
|
||||
updated: true
|
||||
});
|
||||
|
||||
expect(updated.metadata.initial).toBe(true);
|
||||
expect(updated.metadata.updated).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalidateContext', () => {
|
||||
it('should remove context from cache', async () => {
|
||||
await contextManager.getContext('test-id', { test: true });
|
||||
contextManager.invalidateContext('test-id', { test: true });
|
||||
|
||||
// Should be a cache miss
|
||||
await contextManager.getContext('test-id', { test: true });
|
||||
expect(contextManager.stats.invalidations).toBe(1);
|
||||
expect(contextManager.stats.misses).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStats', () => {
|
||||
it('should return current cache statistics', async () => {
|
||||
await contextManager.getContext('test-id', { test: true });
|
||||
const stats = contextManager.getStats();
|
||||
|
||||
expect(stats.hits).toBe(0);
|
||||
expect(stats.misses).toBe(1);
|
||||
expect(stats.invalidations).toBe(0);
|
||||
expect(stats.size).toBe(1);
|
||||
expect(stats.maxSize).toBe(10);
|
||||
expect(stats.ttl).toBe(1000);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,156 +15,157 @@ import { LRUCache } from 'lru-cache';
|
||||
*/
|
||||
|
||||
export class ContextManager {
|
||||
/**
|
||||
* Create a new ContextManager instance
|
||||
* @param {ContextManagerConfig} config - Configuration options
|
||||
*/
|
||||
constructor(config = {}) {
|
||||
this.config = {
|
||||
maxCacheSize: config.maxCacheSize || 1000,
|
||||
ttl: config.ttl || 1000 * 60 * 5, // 5 minutes default
|
||||
maxContextSize: config.maxContextSize || 4000
|
||||
};
|
||||
/**
|
||||
* Create a new ContextManager instance
|
||||
* @param {ContextManagerConfig} config - Configuration options
|
||||
*/
|
||||
constructor(config = {}) {
|
||||
this.config = {
|
||||
maxCacheSize: config.maxCacheSize || 1000,
|
||||
ttl: config.ttl || 1000 * 60 * 5, // 5 minutes default
|
||||
maxContextSize: config.maxContextSize || 4000
|
||||
};
|
||||
|
||||
// Initialize LRU cache for context data
|
||||
this.cache = new LRUCache({
|
||||
max: this.config.maxCacheSize,
|
||||
ttl: this.config.ttl,
|
||||
updateAgeOnGet: true
|
||||
});
|
||||
// Initialize LRU cache for context data
|
||||
this.cache = new LRUCache({
|
||||
max: this.config.maxCacheSize,
|
||||
ttl: this.config.ttl,
|
||||
updateAgeOnGet: true
|
||||
});
|
||||
|
||||
// Cache statistics
|
||||
this.stats = {
|
||||
hits: 0,
|
||||
misses: 0,
|
||||
invalidations: 0
|
||||
};
|
||||
}
|
||||
// Cache statistics
|
||||
this.stats = {
|
||||
hits: 0,
|
||||
misses: 0,
|
||||
invalidations: 0
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new context or retrieve from cache
|
||||
* @param {string} contextId - Unique identifier for the context
|
||||
* @param {Object} metadata - Additional metadata for the context
|
||||
* @returns {Object} Context object with metadata
|
||||
*/
|
||||
async getContext(contextId, metadata = {}) {
|
||||
const cacheKey = this._getCacheKey(contextId, metadata);
|
||||
|
||||
// Try to get from cache first
|
||||
const cached = this.cache.get(cacheKey);
|
||||
if (cached) {
|
||||
this.stats.hits++;
|
||||
return cached;
|
||||
}
|
||||
/**
|
||||
* Create a new context or retrieve from cache
|
||||
* @param {string} contextId - Unique identifier for the context
|
||||
* @param {Object} metadata - Additional metadata for the context
|
||||
* @returns {Object} Context object with metadata
|
||||
*/
|
||||
async getContext(contextId, metadata = {}) {
|
||||
const cacheKey = this._getCacheKey(contextId, metadata);
|
||||
|
||||
this.stats.misses++;
|
||||
|
||||
// Create new context if not in cache
|
||||
const context = {
|
||||
id: contextId,
|
||||
metadata: {
|
||||
...metadata,
|
||||
created: new Date().toISOString()
|
||||
}
|
||||
};
|
||||
// Try to get from cache first
|
||||
const cached = this.cache.get(cacheKey);
|
||||
if (cached) {
|
||||
this.stats.hits++;
|
||||
return cached;
|
||||
}
|
||||
|
||||
// Cache the new context
|
||||
this.cache.set(cacheKey, context);
|
||||
|
||||
return context;
|
||||
}
|
||||
this.stats.misses++;
|
||||
|
||||
/**
|
||||
* Update an existing context
|
||||
* @param {string} contextId - Context identifier
|
||||
* @param {Object} updates - Updates to apply to the context
|
||||
* @returns {Object} Updated context
|
||||
*/
|
||||
async updateContext(contextId, updates) {
|
||||
const context = await this.getContext(contextId);
|
||||
|
||||
// Apply updates to context
|
||||
Object.assign(context.metadata, updates);
|
||||
|
||||
// Update cache
|
||||
const cacheKey = this._getCacheKey(contextId, context.metadata);
|
||||
this.cache.set(cacheKey, context);
|
||||
|
||||
return context;
|
||||
}
|
||||
// Create new context if not in cache
|
||||
const context = {
|
||||
id: contextId,
|
||||
metadata: {
|
||||
...metadata,
|
||||
created: new Date().toISOString()
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Invalidate a context in the cache
|
||||
* @param {string} contextId - Context identifier
|
||||
* @param {Object} metadata - Metadata used in the cache key
|
||||
*/
|
||||
invalidateContext(contextId, metadata = {}) {
|
||||
const cacheKey = this._getCacheKey(contextId, metadata);
|
||||
this.cache.delete(cacheKey);
|
||||
this.stats.invalidations++;
|
||||
}
|
||||
// Cache the new context
|
||||
this.cache.set(cacheKey, context);
|
||||
|
||||
/**
|
||||
* Get cached data associated with a specific key.
|
||||
* Increments cache hit stats if found.
|
||||
* @param {string} key - The cache key.
|
||||
* @returns {any | undefined} The cached data or undefined if not found/expired.
|
||||
*/
|
||||
getCachedData(key) {
|
||||
const cached = this.cache.get(key);
|
||||
if (cached !== undefined) { // Check for undefined specifically, as null/false might be valid cached values
|
||||
this.stats.hits++;
|
||||
return cached;
|
||||
}
|
||||
this.stats.misses++;
|
||||
return undefined;
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set data in the cache with a specific key.
|
||||
* @param {string} key - The cache key.
|
||||
* @param {any} data - The data to cache.
|
||||
*/
|
||||
setCachedData(key, data) {
|
||||
this.cache.set(key, data);
|
||||
}
|
||||
/**
|
||||
* Update an existing context
|
||||
* @param {string} contextId - Context identifier
|
||||
* @param {Object} updates - Updates to apply to the context
|
||||
* @returns {Object} Updated context
|
||||
*/
|
||||
async updateContext(contextId, updates) {
|
||||
const context = await this.getContext(contextId);
|
||||
|
||||
/**
|
||||
* Invalidate a specific cache key.
|
||||
* Increments invalidation stats.
|
||||
* @param {string} key - The cache key to invalidate.
|
||||
*/
|
||||
invalidateCacheKey(key) {
|
||||
this.cache.delete(key);
|
||||
this.stats.invalidations++;
|
||||
}
|
||||
// Apply updates to context
|
||||
Object.assign(context.metadata, updates);
|
||||
|
||||
/**
|
||||
* Get cache statistics
|
||||
* @returns {Object} Cache statistics
|
||||
*/
|
||||
getStats() {
|
||||
return {
|
||||
hits: this.stats.hits,
|
||||
misses: this.stats.misses,
|
||||
invalidations: this.stats.invalidations,
|
||||
size: this.cache.size,
|
||||
maxSize: this.config.maxCacheSize,
|
||||
ttl: this.config.ttl
|
||||
};
|
||||
}
|
||||
// Update cache
|
||||
const cacheKey = this._getCacheKey(contextId, context.metadata);
|
||||
this.cache.set(cacheKey, context);
|
||||
|
||||
/**
|
||||
* Generate a cache key from context ID and metadata
|
||||
* @private
|
||||
* @deprecated No longer used for direct cache key generation outside the manager.
|
||||
* Prefer generating specific keys in calling functions.
|
||||
*/
|
||||
_getCacheKey(contextId, metadata) {
|
||||
// Kept for potential backward compatibility or internal use if needed later.
|
||||
return `${contextId}:${JSON.stringify(metadata)}`;
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate a context in the cache
|
||||
* @param {string} contextId - Context identifier
|
||||
* @param {Object} metadata - Metadata used in the cache key
|
||||
*/
|
||||
invalidateContext(contextId, metadata = {}) {
|
||||
const cacheKey = this._getCacheKey(contextId, metadata);
|
||||
this.cache.delete(cacheKey);
|
||||
this.stats.invalidations++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached data associated with a specific key.
|
||||
* Increments cache hit stats if found.
|
||||
* @param {string} key - The cache key.
|
||||
* @returns {any | undefined} The cached data or undefined if not found/expired.
|
||||
*/
|
||||
getCachedData(key) {
|
||||
const cached = this.cache.get(key);
|
||||
if (cached !== undefined) {
|
||||
// Check for undefined specifically, as null/false might be valid cached values
|
||||
this.stats.hits++;
|
||||
return cached;
|
||||
}
|
||||
this.stats.misses++;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set data in the cache with a specific key.
|
||||
* @param {string} key - The cache key.
|
||||
* @param {any} data - The data to cache.
|
||||
*/
|
||||
setCachedData(key, data) {
|
||||
this.cache.set(key, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate a specific cache key.
|
||||
* Increments invalidation stats.
|
||||
* @param {string} key - The cache key to invalidate.
|
||||
*/
|
||||
invalidateCacheKey(key) {
|
||||
this.cache.delete(key);
|
||||
this.stats.invalidations++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache statistics
|
||||
* @returns {Object} Cache statistics
|
||||
*/
|
||||
getStats() {
|
||||
return {
|
||||
hits: this.stats.hits,
|
||||
misses: this.stats.misses,
|
||||
invalidations: this.stats.invalidations,
|
||||
size: this.cache.size,
|
||||
maxSize: this.config.maxCacheSize,
|
||||
ttl: this.config.ttl
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a cache key from context ID and metadata
|
||||
* @private
|
||||
* @deprecated No longer used for direct cache key generation outside the manager.
|
||||
* Prefer generating specific keys in calling functions.
|
||||
*/
|
||||
_getCacheKey(contextId, metadata) {
|
||||
// Kept for potential backward compatibility or internal use if needed later.
|
||||
return `${contextId}:${JSON.stringify(metadata)}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Export a singleton instance with default config
|
||||
export const contextManager = new ContextManager();
|
||||
export const contextManager = new ContextManager();
|
||||
|
||||
@@ -5,11 +5,14 @@
|
||||
|
||||
import { addDependency } from '../../../../scripts/modules/dependency-manager.js';
|
||||
import { findTasksJsonPath } from '../utils/path-utils.js';
|
||||
import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js';
|
||||
import {
|
||||
enableSilentMode,
|
||||
disableSilentMode
|
||||
} from '../../../../scripts/modules/utils.js';
|
||||
|
||||
/**
|
||||
* Direct function wrapper for addDependency with error handling.
|
||||
*
|
||||
*
|
||||
* @param {Object} args - Command arguments
|
||||
* @param {string|number} args.id - Task ID to add dependency to
|
||||
* @param {string|number} args.dependsOn - Task ID that will become a dependency
|
||||
@@ -19,67 +22,75 @@ import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules
|
||||
* @returns {Promise<Object>} - Result object with success status and data/error information
|
||||
*/
|
||||
export async function addDependencyDirect(args, log) {
|
||||
try {
|
||||
log.info(`Adding dependency with args: ${JSON.stringify(args)}`);
|
||||
|
||||
// Validate required parameters
|
||||
if (!args.id) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'INPUT_VALIDATION_ERROR',
|
||||
message: 'Task ID (id) is required'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (!args.dependsOn) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'INPUT_VALIDATION_ERROR',
|
||||
message: 'Dependency ID (dependsOn) is required'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Find the tasks.json path
|
||||
const tasksPath = findTasksJsonPath(args, log);
|
||||
|
||||
// Format IDs for the core function
|
||||
const taskId = args.id.includes && args.id.includes('.') ? args.id : parseInt(args.id, 10);
|
||||
const dependencyId = args.dependsOn.includes && args.dependsOn.includes('.') ? args.dependsOn : parseInt(args.dependsOn, 10);
|
||||
|
||||
log.info(`Adding dependency: task ${taskId} will depend on ${dependencyId}`);
|
||||
|
||||
// Enable silent mode to prevent console logs from interfering with JSON response
|
||||
enableSilentMode();
|
||||
|
||||
// Call the core function
|
||||
await addDependency(tasksPath, taskId, dependencyId);
|
||||
|
||||
// Restore normal logging
|
||||
disableSilentMode();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
message: `Successfully added dependency: Task ${taskId} now depends on ${dependencyId}`,
|
||||
taskId: taskId,
|
||||
dependencyId: dependencyId
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
// Make sure to restore normal logging even if there's an error
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Error in addDependencyDirect: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'CORE_FUNCTION_ERROR',
|
||||
message: error.message
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
try {
|
||||
log.info(`Adding dependency with args: ${JSON.stringify(args)}`);
|
||||
|
||||
// Validate required parameters
|
||||
if (!args.id) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'INPUT_VALIDATION_ERROR',
|
||||
message: 'Task ID (id) is required'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (!args.dependsOn) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'INPUT_VALIDATION_ERROR',
|
||||
message: 'Dependency ID (dependsOn) is required'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Find the tasks.json path
|
||||
const tasksPath = findTasksJsonPath(args, log);
|
||||
|
||||
// Format IDs for the core function
|
||||
const taskId =
|
||||
args.id.includes && args.id.includes('.')
|
||||
? args.id
|
||||
: parseInt(args.id, 10);
|
||||
const dependencyId =
|
||||
args.dependsOn.includes && args.dependsOn.includes('.')
|
||||
? args.dependsOn
|
||||
: parseInt(args.dependsOn, 10);
|
||||
|
||||
log.info(
|
||||
`Adding dependency: task ${taskId} will depend on ${dependencyId}`
|
||||
);
|
||||
|
||||
// Enable silent mode to prevent console logs from interfering with JSON response
|
||||
enableSilentMode();
|
||||
|
||||
// Call the core function
|
||||
await addDependency(tasksPath, taskId, dependencyId);
|
||||
|
||||
// Restore normal logging
|
||||
disableSilentMode();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
message: `Successfully added dependency: Task ${taskId} now depends on ${dependencyId}`,
|
||||
taskId: taskId,
|
||||
dependencyId: dependencyId
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
// Make sure to restore normal logging even if there's an error
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Error in addDependencyDirect: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'CORE_FUNCTION_ERROR',
|
||||
message: error.message
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
|
||||
import { addSubtask } from '../../../../scripts/modules/task-manager.js';
|
||||
import { findTasksJsonPath } from '../utils/path-utils.js';
|
||||
import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js';
|
||||
import {
|
||||
enableSilentMode,
|
||||
disableSilentMode
|
||||
} from '../../../../scripts/modules/utils.js';
|
||||
|
||||
/**
|
||||
* Add a subtask to an existing task
|
||||
@@ -23,106 +26,118 @@ import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules
|
||||
* @returns {Promise<{success: boolean, data?: Object, error?: string}>}
|
||||
*/
|
||||
export async function addSubtaskDirect(args, log) {
|
||||
try {
|
||||
log.info(`Adding subtask with args: ${JSON.stringify(args)}`);
|
||||
|
||||
if (!args.id) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'INPUT_VALIDATION_ERROR',
|
||||
message: 'Parent task ID is required'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Either taskId or title must be provided
|
||||
if (!args.taskId && !args.title) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'INPUT_VALIDATION_ERROR',
|
||||
message: 'Either taskId or title must be provided'
|
||||
}
|
||||
};
|
||||
}
|
||||
try {
|
||||
log.info(`Adding subtask with args: ${JSON.stringify(args)}`);
|
||||
|
||||
// Find the tasks.json path
|
||||
const tasksPath = findTasksJsonPath(args, log);
|
||||
|
||||
// Parse dependencies if provided
|
||||
let dependencies = [];
|
||||
if (args.dependencies) {
|
||||
dependencies = args.dependencies.split(',').map(id => {
|
||||
// Handle both regular IDs and dot notation
|
||||
return id.includes('.') ? id.trim() : parseInt(id.trim(), 10);
|
||||
});
|
||||
}
|
||||
|
||||
// Convert existingTaskId to a number if provided
|
||||
const existingTaskId = args.taskId ? parseInt(args.taskId, 10) : null;
|
||||
|
||||
// Convert parent ID to a number
|
||||
const parentId = parseInt(args.id, 10);
|
||||
|
||||
// Determine if we should generate files
|
||||
const generateFiles = !args.skipGenerate;
|
||||
|
||||
// Enable silent mode to prevent console logs from interfering with JSON response
|
||||
enableSilentMode();
|
||||
|
||||
// Case 1: Convert existing task to subtask
|
||||
if (existingTaskId) {
|
||||
log.info(`Converting task ${existingTaskId} to a subtask of ${parentId}`);
|
||||
const result = await addSubtask(tasksPath, parentId, existingTaskId, null, generateFiles);
|
||||
|
||||
// Restore normal logging
|
||||
disableSilentMode();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
message: `Task ${existingTaskId} successfully converted to a subtask of task ${parentId}`,
|
||||
subtask: result
|
||||
}
|
||||
};
|
||||
}
|
||||
// Case 2: Create new subtask
|
||||
else {
|
||||
log.info(`Creating new subtask for parent task ${parentId}`);
|
||||
|
||||
const newSubtaskData = {
|
||||
title: args.title,
|
||||
description: args.description || '',
|
||||
details: args.details || '',
|
||||
status: args.status || 'pending',
|
||||
dependencies: dependencies
|
||||
};
|
||||
|
||||
const result = await addSubtask(tasksPath, parentId, null, newSubtaskData, generateFiles);
|
||||
|
||||
// Restore normal logging
|
||||
disableSilentMode();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
message: `New subtask ${parentId}.${result.id} successfully created`,
|
||||
subtask: result
|
||||
}
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
// Make sure to restore normal logging even if there's an error
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Error in addSubtaskDirect: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'CORE_FUNCTION_ERROR',
|
||||
message: error.message
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
if (!args.id) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'INPUT_VALIDATION_ERROR',
|
||||
message: 'Parent task ID is required'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Either taskId or title must be provided
|
||||
if (!args.taskId && !args.title) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'INPUT_VALIDATION_ERROR',
|
||||
message: 'Either taskId or title must be provided'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Find the tasks.json path
|
||||
const tasksPath = findTasksJsonPath(args, log);
|
||||
|
||||
// Parse dependencies if provided
|
||||
let dependencies = [];
|
||||
if (args.dependencies) {
|
||||
dependencies = args.dependencies.split(',').map((id) => {
|
||||
// Handle both regular IDs and dot notation
|
||||
return id.includes('.') ? id.trim() : parseInt(id.trim(), 10);
|
||||
});
|
||||
}
|
||||
|
||||
// Convert existingTaskId to a number if provided
|
||||
const existingTaskId = args.taskId ? parseInt(args.taskId, 10) : null;
|
||||
|
||||
// Convert parent ID to a number
|
||||
const parentId = parseInt(args.id, 10);
|
||||
|
||||
// Determine if we should generate files
|
||||
const generateFiles = !args.skipGenerate;
|
||||
|
||||
// Enable silent mode to prevent console logs from interfering with JSON response
|
||||
enableSilentMode();
|
||||
|
||||
// Case 1: Convert existing task to subtask
|
||||
if (existingTaskId) {
|
||||
log.info(`Converting task ${existingTaskId} to a subtask of ${parentId}`);
|
||||
const result = await addSubtask(
|
||||
tasksPath,
|
||||
parentId,
|
||||
existingTaskId,
|
||||
null,
|
||||
generateFiles
|
||||
);
|
||||
|
||||
// Restore normal logging
|
||||
disableSilentMode();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
message: `Task ${existingTaskId} successfully converted to a subtask of task ${parentId}`,
|
||||
subtask: result
|
||||
}
|
||||
};
|
||||
}
|
||||
// Case 2: Create new subtask
|
||||
else {
|
||||
log.info(`Creating new subtask for parent task ${parentId}`);
|
||||
|
||||
const newSubtaskData = {
|
||||
title: args.title,
|
||||
description: args.description || '',
|
||||
details: args.details || '',
|
||||
status: args.status || 'pending',
|
||||
dependencies: dependencies
|
||||
};
|
||||
|
||||
const result = await addSubtask(
|
||||
tasksPath,
|
||||
parentId,
|
||||
null,
|
||||
newSubtaskData,
|
||||
generateFiles
|
||||
);
|
||||
|
||||
// Restore normal logging
|
||||
disableSilentMode();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
message: `New subtask ${parentId}.${result.id} successfully created`,
|
||||
subtask: result
|
||||
}
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
// Make sure to restore normal logging even if there's an error
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Error in addSubtaskDirect: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'CORE_FUNCTION_ERROR',
|
||||
message: error.message
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,19 @@
|
||||
|
||||
import { addTask } from '../../../../scripts/modules/task-manager.js';
|
||||
import { findTasksJsonPath } from '../utils/path-utils.js';
|
||||
import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js';
|
||||
import { getAnthropicClientForMCP, getModelConfig } from '../utils/ai-client-utils.js';
|
||||
import { _buildAddTaskPrompt, parseTaskJsonResponse, _handleAnthropicStream } from '../../../../scripts/modules/ai-services.js';
|
||||
import {
|
||||
enableSilentMode,
|
||||
disableSilentMode
|
||||
} from '../../../../scripts/modules/utils.js';
|
||||
import {
|
||||
getAnthropicClientForMCP,
|
||||
getModelConfig
|
||||
} from '../utils/ai-client-utils.js';
|
||||
import {
|
||||
_buildAddTaskPrompt,
|
||||
parseTaskJsonResponse,
|
||||
_handleAnthropicStream
|
||||
} from '../../../../scripts/modules/ai-services.js';
|
||||
|
||||
/**
|
||||
* Direct function wrapper for adding a new task with error handling.
|
||||
@@ -24,153 +34,162 @@ import { _buildAddTaskPrompt, parseTaskJsonResponse, _handleAnthropicStream } fr
|
||||
* @returns {Promise<Object>} - Result object { success: boolean, data?: any, error?: { code: string, message: string } }
|
||||
*/
|
||||
export async function addTaskDirect(args, log, context = {}) {
|
||||
try {
|
||||
// Enable silent mode to prevent console logs from interfering with JSON response
|
||||
enableSilentMode();
|
||||
|
||||
// Find the tasks.json path
|
||||
const tasksPath = findTasksJsonPath(args, log);
|
||||
|
||||
// Check required parameters
|
||||
if (!args.prompt) {
|
||||
log.error('Missing required parameter: prompt');
|
||||
disableSilentMode();
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'MISSING_PARAMETER',
|
||||
message: 'The prompt parameter is required for adding a task'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Extract and prepare parameters
|
||||
const prompt = args.prompt;
|
||||
const dependencies = Array.isArray(args.dependencies)
|
||||
? args.dependencies
|
||||
: (args.dependencies ? String(args.dependencies).split(',').map(id => parseInt(id.trim(), 10)) : []);
|
||||
const priority = args.priority || 'medium';
|
||||
|
||||
log.info(`Adding new task with prompt: "${prompt}", dependencies: [${dependencies.join(', ')}], priority: ${priority}`);
|
||||
|
||||
// Extract context parameters for advanced functionality
|
||||
// Commenting out reportProgress extraction
|
||||
// const { reportProgress, session } = context;
|
||||
const { session } = context; // Keep session
|
||||
try {
|
||||
// Enable silent mode to prevent console logs from interfering with JSON response
|
||||
enableSilentMode();
|
||||
|
||||
// Initialize AI client with session environment
|
||||
let localAnthropic;
|
||||
try {
|
||||
localAnthropic = getAnthropicClientForMCP(session, log);
|
||||
} catch (error) {
|
||||
log.error(`Failed to initialize Anthropic client: ${error.message}`);
|
||||
disableSilentMode();
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'AI_CLIENT_ERROR',
|
||||
message: `Cannot initialize AI client: ${error.message}`
|
||||
}
|
||||
};
|
||||
}
|
||||
// Find the tasks.json path
|
||||
const tasksPath = findTasksJsonPath(args, log);
|
||||
|
||||
// Get model configuration from session
|
||||
const modelConfig = getModelConfig(session);
|
||||
// Check required parameters
|
||||
if (!args.prompt) {
|
||||
log.error('Missing required parameter: prompt');
|
||||
disableSilentMode();
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'MISSING_PARAMETER',
|
||||
message: 'The prompt parameter is required for adding a task'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Read existing tasks to provide context
|
||||
let tasksData;
|
||||
try {
|
||||
const fs = await import('fs');
|
||||
tasksData = JSON.parse(fs.readFileSync(tasksPath, 'utf8'));
|
||||
} catch (error) {
|
||||
log.warn(`Could not read existing tasks for context: ${error.message}`);
|
||||
tasksData = { tasks: [] };
|
||||
}
|
||||
// Extract and prepare parameters
|
||||
const prompt = args.prompt;
|
||||
const dependencies = Array.isArray(args.dependencies)
|
||||
? args.dependencies
|
||||
: args.dependencies
|
||||
? String(args.dependencies)
|
||||
.split(',')
|
||||
.map((id) => parseInt(id.trim(), 10))
|
||||
: [];
|
||||
const priority = args.priority || 'medium';
|
||||
|
||||
// Build prompts for AI
|
||||
const { systemPrompt, userPrompt } = _buildAddTaskPrompt(prompt, tasksData.tasks);
|
||||
log.info(
|
||||
`Adding new task with prompt: "${prompt}", dependencies: [${dependencies.join(', ')}], priority: ${priority}`
|
||||
);
|
||||
|
||||
// Make the AI call using the streaming helper
|
||||
let responseText;
|
||||
try {
|
||||
responseText = await _handleAnthropicStream(
|
||||
localAnthropic,
|
||||
{
|
||||
model: modelConfig.model,
|
||||
max_tokens: modelConfig.maxTokens,
|
||||
temperature: modelConfig.temperature,
|
||||
messages: [{ role: "user", content: userPrompt }],
|
||||
system: systemPrompt
|
||||
},
|
||||
{
|
||||
// reportProgress: context.reportProgress, // Commented out to prevent Cursor stroking out
|
||||
mcpLog: log
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(`AI processing failed: ${error.message}`);
|
||||
disableSilentMode();
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'AI_PROCESSING_ERROR',
|
||||
message: `Failed to generate task with AI: ${error.message}`
|
||||
}
|
||||
};
|
||||
}
|
||||
// Extract context parameters for advanced functionality
|
||||
// Commenting out reportProgress extraction
|
||||
// const { reportProgress, session } = context;
|
||||
const { session } = context; // Keep session
|
||||
|
||||
// Parse the AI response
|
||||
let taskDataFromAI;
|
||||
try {
|
||||
taskDataFromAI = parseTaskJsonResponse(responseText);
|
||||
} catch (error) {
|
||||
log.error(`Failed to parse AI response: ${error.message}`);
|
||||
disableSilentMode();
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'RESPONSE_PARSING_ERROR',
|
||||
message: `Failed to parse AI response: ${error.message}`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Call the addTask function with 'json' outputFormat to prevent console output when called via MCP
|
||||
const newTaskId = await addTask(
|
||||
tasksPath,
|
||||
prompt,
|
||||
dependencies,
|
||||
priority,
|
||||
{
|
||||
// reportProgress, // Commented out
|
||||
mcpLog: log,
|
||||
session,
|
||||
taskDataFromAI // Pass the parsed AI result
|
||||
},
|
||||
'json'
|
||||
);
|
||||
|
||||
// Restore normal logging
|
||||
disableSilentMode();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
taskId: newTaskId,
|
||||
message: `Successfully added new task #${newTaskId}`
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
// Make sure to restore normal logging even if there's an error
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Error in addTaskDirect: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'ADD_TASK_ERROR',
|
||||
message: error.message
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
// Initialize AI client with session environment
|
||||
let localAnthropic;
|
||||
try {
|
||||
localAnthropic = getAnthropicClientForMCP(session, log);
|
||||
} catch (error) {
|
||||
log.error(`Failed to initialize Anthropic client: ${error.message}`);
|
||||
disableSilentMode();
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'AI_CLIENT_ERROR',
|
||||
message: `Cannot initialize AI client: ${error.message}`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Get model configuration from session
|
||||
const modelConfig = getModelConfig(session);
|
||||
|
||||
// Read existing tasks to provide context
|
||||
let tasksData;
|
||||
try {
|
||||
const fs = await import('fs');
|
||||
tasksData = JSON.parse(fs.readFileSync(tasksPath, 'utf8'));
|
||||
} catch (error) {
|
||||
log.warn(`Could not read existing tasks for context: ${error.message}`);
|
||||
tasksData = { tasks: [] };
|
||||
}
|
||||
|
||||
// Build prompts for AI
|
||||
const { systemPrompt, userPrompt } = _buildAddTaskPrompt(
|
||||
prompt,
|
||||
tasksData.tasks
|
||||
);
|
||||
|
||||
// Make the AI call using the streaming helper
|
||||
let responseText;
|
||||
try {
|
||||
responseText = await _handleAnthropicStream(
|
||||
localAnthropic,
|
||||
{
|
||||
model: modelConfig.model,
|
||||
max_tokens: modelConfig.maxTokens,
|
||||
temperature: modelConfig.temperature,
|
||||
messages: [{ role: 'user', content: userPrompt }],
|
||||
system: systemPrompt
|
||||
},
|
||||
{
|
||||
// reportProgress: context.reportProgress, // Commented out to prevent Cursor stroking out
|
||||
mcpLog: log
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(`AI processing failed: ${error.message}`);
|
||||
disableSilentMode();
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'AI_PROCESSING_ERROR',
|
||||
message: `Failed to generate task with AI: ${error.message}`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Parse the AI response
|
||||
let taskDataFromAI;
|
||||
try {
|
||||
taskDataFromAI = parseTaskJsonResponse(responseText);
|
||||
} catch (error) {
|
||||
log.error(`Failed to parse AI response: ${error.message}`);
|
||||
disableSilentMode();
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'RESPONSE_PARSING_ERROR',
|
||||
message: `Failed to parse AI response: ${error.message}`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Call the addTask function with 'json' outputFormat to prevent console output when called via MCP
|
||||
const newTaskId = await addTask(
|
||||
tasksPath,
|
||||
prompt,
|
||||
dependencies,
|
||||
priority,
|
||||
{
|
||||
// reportProgress, // Commented out
|
||||
mcpLog: log,
|
||||
session,
|
||||
taskDataFromAI // Pass the parsed AI result
|
||||
},
|
||||
'json'
|
||||
);
|
||||
|
||||
// Restore normal logging
|
||||
disableSilentMode();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
taskId: newTaskId,
|
||||
message: `Successfully added new task #${newTaskId}`
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
// Make sure to restore normal logging even if there's an error
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Error in addTaskDirect: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'ADD_TASK_ERROR',
|
||||
message: error.message
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,12 @@
|
||||
|
||||
import { analyzeTaskComplexity } from '../../../../scripts/modules/task-manager.js';
|
||||
import { findTasksJsonPath } from '../utils/path-utils.js';
|
||||
import { enableSilentMode, disableSilentMode, isSilentMode, readJSON } from '../../../../scripts/modules/utils.js';
|
||||
import {
|
||||
enableSilentMode,
|
||||
disableSilentMode,
|
||||
isSilentMode,
|
||||
readJSON
|
||||
} from '../../../../scripts/modules/utils.js';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
@@ -22,135 +27,142 @@ import path from 'path';
|
||||
* @returns {Promise<{success: boolean, data?: Object, error?: {code: string, message: string}}>}
|
||||
*/
|
||||
export async function analyzeTaskComplexityDirect(args, log, context = {}) {
|
||||
const { session } = context; // Only extract session, not reportProgress
|
||||
|
||||
try {
|
||||
log.info(`Analyzing task complexity with args: ${JSON.stringify(args)}`);
|
||||
|
||||
// Find the tasks.json path
|
||||
const tasksPath = findTasksJsonPath(args, log);
|
||||
|
||||
// Determine output path
|
||||
let outputPath = args.output || 'scripts/task-complexity-report.json';
|
||||
if (!path.isAbsolute(outputPath) && args.projectRoot) {
|
||||
outputPath = path.join(args.projectRoot, outputPath);
|
||||
}
|
||||
|
||||
log.info(`Analyzing task complexity from: ${tasksPath}`);
|
||||
log.info(`Output report will be saved to: ${outputPath}`);
|
||||
|
||||
if (args.research) {
|
||||
log.info('Using Perplexity AI for research-backed complexity analysis');
|
||||
}
|
||||
|
||||
// Create options object for analyzeTaskComplexity
|
||||
const options = {
|
||||
file: tasksPath,
|
||||
output: outputPath,
|
||||
model: args.model,
|
||||
threshold: args.threshold,
|
||||
research: args.research === true
|
||||
};
|
||||
|
||||
// Enable silent mode to prevent console logs from interfering with JSON response
|
||||
const wasSilent = isSilentMode();
|
||||
if (!wasSilent) {
|
||||
enableSilentMode();
|
||||
}
|
||||
|
||||
// Create a logWrapper that matches the expected mcpLog interface as specified in utilities.mdc
|
||||
const logWrapper = {
|
||||
info: (message, ...args) => log.info(message, ...args),
|
||||
warn: (message, ...args) => log.warn(message, ...args),
|
||||
error: (message, ...args) => log.error(message, ...args),
|
||||
debug: (message, ...args) => log.debug && log.debug(message, ...args),
|
||||
success: (message, ...args) => log.info(message, ...args) // Map success to info
|
||||
};
|
||||
|
||||
try {
|
||||
// Call the core function with session and logWrapper as mcpLog
|
||||
await analyzeTaskComplexity(options, {
|
||||
session,
|
||||
mcpLog: logWrapper // Use the wrapper instead of passing log directly
|
||||
});
|
||||
} catch (error) {
|
||||
log.error(`Error in analyzeTaskComplexity: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'ANALYZE_ERROR',
|
||||
message: `Error running complexity analysis: ${error.message}`
|
||||
}
|
||||
};
|
||||
} finally {
|
||||
// Always restore normal logging in finally block, but only if we enabled it
|
||||
if (!wasSilent) {
|
||||
disableSilentMode();
|
||||
}
|
||||
}
|
||||
|
||||
// Verify the report file was created
|
||||
if (!fs.existsSync(outputPath)) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'ANALYZE_ERROR',
|
||||
message: 'Analysis completed but no report file was created'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Read the report file
|
||||
let report;
|
||||
try {
|
||||
report = JSON.parse(fs.readFileSync(outputPath, 'utf8'));
|
||||
|
||||
// Important: Handle different report formats
|
||||
// The core function might return an array or an object with a complexityAnalysis property
|
||||
const analysisArray = Array.isArray(report) ? report :
|
||||
(report.complexityAnalysis || []);
|
||||
|
||||
// Count tasks by complexity
|
||||
const highComplexityTasks = analysisArray.filter(t => t.complexityScore >= 8).length;
|
||||
const mediumComplexityTasks = analysisArray.filter(t => t.complexityScore >= 5 && t.complexityScore < 8).length;
|
||||
const lowComplexityTasks = analysisArray.filter(t => t.complexityScore < 5).length;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
message: `Task complexity analysis complete. Report saved to ${outputPath}`,
|
||||
reportPath: outputPath,
|
||||
reportSummary: {
|
||||
taskCount: analysisArray.length,
|
||||
highComplexityTasks,
|
||||
mediumComplexityTasks,
|
||||
lowComplexityTasks
|
||||
}
|
||||
}
|
||||
};
|
||||
} catch (parseError) {
|
||||
log.error(`Error parsing report file: ${parseError.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'REPORT_PARSE_ERROR',
|
||||
message: `Error parsing complexity report: ${parseError.message}`
|
||||
}
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
// Make sure to restore normal logging even if there's an error
|
||||
if (isSilentMode()) {
|
||||
disableSilentMode();
|
||||
}
|
||||
|
||||
log.error(`Error in analyzeTaskComplexityDirect: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'CORE_FUNCTION_ERROR',
|
||||
message: error.message
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
const { session } = context; // Only extract session, not reportProgress
|
||||
|
||||
try {
|
||||
log.info(`Analyzing task complexity with args: ${JSON.stringify(args)}`);
|
||||
|
||||
// Find the tasks.json path
|
||||
const tasksPath = findTasksJsonPath(args, log);
|
||||
|
||||
// Determine output path
|
||||
let outputPath = args.output || 'scripts/task-complexity-report.json';
|
||||
if (!path.isAbsolute(outputPath) && args.projectRoot) {
|
||||
outputPath = path.join(args.projectRoot, outputPath);
|
||||
}
|
||||
|
||||
log.info(`Analyzing task complexity from: ${tasksPath}`);
|
||||
log.info(`Output report will be saved to: ${outputPath}`);
|
||||
|
||||
if (args.research) {
|
||||
log.info('Using Perplexity AI for research-backed complexity analysis');
|
||||
}
|
||||
|
||||
// Create options object for analyzeTaskComplexity
|
||||
const options = {
|
||||
file: tasksPath,
|
||||
output: outputPath,
|
||||
model: args.model,
|
||||
threshold: args.threshold,
|
||||
research: args.research === true
|
||||
};
|
||||
|
||||
// Enable silent mode to prevent console logs from interfering with JSON response
|
||||
const wasSilent = isSilentMode();
|
||||
if (!wasSilent) {
|
||||
enableSilentMode();
|
||||
}
|
||||
|
||||
// Create a logWrapper that matches the expected mcpLog interface as specified in utilities.mdc
|
||||
const logWrapper = {
|
||||
info: (message, ...args) => log.info(message, ...args),
|
||||
warn: (message, ...args) => log.warn(message, ...args),
|
||||
error: (message, ...args) => log.error(message, ...args),
|
||||
debug: (message, ...args) => log.debug && log.debug(message, ...args),
|
||||
success: (message, ...args) => log.info(message, ...args) // Map success to info
|
||||
};
|
||||
|
||||
try {
|
||||
// Call the core function with session and logWrapper as mcpLog
|
||||
await analyzeTaskComplexity(options, {
|
||||
session,
|
||||
mcpLog: logWrapper // Use the wrapper instead of passing log directly
|
||||
});
|
||||
} catch (error) {
|
||||
log.error(`Error in analyzeTaskComplexity: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'ANALYZE_ERROR',
|
||||
message: `Error running complexity analysis: ${error.message}`
|
||||
}
|
||||
};
|
||||
} finally {
|
||||
// Always restore normal logging in finally block, but only if we enabled it
|
||||
if (!wasSilent) {
|
||||
disableSilentMode();
|
||||
}
|
||||
}
|
||||
|
||||
// Verify the report file was created
|
||||
if (!fs.existsSync(outputPath)) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'ANALYZE_ERROR',
|
||||
message: 'Analysis completed but no report file was created'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Read the report file
|
||||
let report;
|
||||
try {
|
||||
report = JSON.parse(fs.readFileSync(outputPath, 'utf8'));
|
||||
|
||||
// Important: Handle different report formats
|
||||
// The core function might return an array or an object with a complexityAnalysis property
|
||||
const analysisArray = Array.isArray(report)
|
||||
? report
|
||||
: report.complexityAnalysis || [];
|
||||
|
||||
// Count tasks by complexity
|
||||
const highComplexityTasks = analysisArray.filter(
|
||||
(t) => t.complexityScore >= 8
|
||||
).length;
|
||||
const mediumComplexityTasks = analysisArray.filter(
|
||||
(t) => t.complexityScore >= 5 && t.complexityScore < 8
|
||||
).length;
|
||||
const lowComplexityTasks = analysisArray.filter(
|
||||
(t) => t.complexityScore < 5
|
||||
).length;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
message: `Task complexity analysis complete. Report saved to ${outputPath}`,
|
||||
reportPath: outputPath,
|
||||
reportSummary: {
|
||||
taskCount: analysisArray.length,
|
||||
highComplexityTasks,
|
||||
mediumComplexityTasks,
|
||||
lowComplexityTasks
|
||||
}
|
||||
}
|
||||
};
|
||||
} catch (parseError) {
|
||||
log.error(`Error parsing report file: ${parseError.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'REPORT_PARSE_ERROR',
|
||||
message: `Error parsing complexity report: ${parseError.message}`
|
||||
}
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
// Make sure to restore normal logging even if there's an error
|
||||
if (isSilentMode()) {
|
||||
disableSilentMode();
|
||||
}
|
||||
|
||||
log.error(`Error in analyzeTaskComplexityDirect: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'CORE_FUNCTION_ERROR',
|
||||
message: error.message
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,21 +12,21 @@ import { contextManager } from '../context-manager.js';
|
||||
* @returns {Object} - Cache statistics
|
||||
*/
|
||||
export async function getCacheStatsDirect(args, log) {
|
||||
try {
|
||||
log.info('Retrieving cache statistics');
|
||||
const stats = contextManager.getStats();
|
||||
return {
|
||||
success: true,
|
||||
data: stats
|
||||
};
|
||||
} catch (error) {
|
||||
log.error(`Error getting cache stats: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'CACHE_STATS_ERROR',
|
||||
message: error.message || 'Unknown error occurred'
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
try {
|
||||
log.info('Retrieving cache statistics');
|
||||
const stats = contextManager.getStats();
|
||||
return {
|
||||
success: true,
|
||||
data: stats
|
||||
};
|
||||
} catch (error) {
|
||||
log.error(`Error getting cache stats: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'CACHE_STATS_ERROR',
|
||||
message: error.message || 'Unknown error occurred'
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
|
||||
import { clearSubtasks } from '../../../../scripts/modules/task-manager.js';
|
||||
import { findTasksJsonPath } from '../utils/path-utils.js';
|
||||
import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js';
|
||||
import {
|
||||
enableSilentMode,
|
||||
disableSilentMode
|
||||
} from '../../../../scripts/modules/utils.js';
|
||||
import fs from 'fs';
|
||||
|
||||
/**
|
||||
@@ -18,95 +21,96 @@ import fs from 'fs';
|
||||
* @returns {Promise<{success: boolean, data?: Object, error?: {code: string, message: string}}>}
|
||||
*/
|
||||
export async function clearSubtasksDirect(args, log) {
|
||||
try {
|
||||
log.info(`Clearing subtasks with args: ${JSON.stringify(args)}`);
|
||||
|
||||
// Either id or all must be provided
|
||||
if (!args.id && !args.all) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'INPUT_VALIDATION_ERROR',
|
||||
message: 'Either task IDs with id parameter or all parameter must be provided'
|
||||
}
|
||||
};
|
||||
}
|
||||
try {
|
||||
log.info(`Clearing subtasks with args: ${JSON.stringify(args)}`);
|
||||
|
||||
// Find the tasks.json path
|
||||
const tasksPath = findTasksJsonPath(args, log);
|
||||
|
||||
// Check if tasks.json exists
|
||||
if (!fs.existsSync(tasksPath)) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'FILE_NOT_FOUND_ERROR',
|
||||
message: `Tasks file not found at ${tasksPath}`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let taskIds;
|
||||
|
||||
// If all is specified, get all task IDs
|
||||
if (args.all) {
|
||||
log.info('Clearing subtasks from all tasks');
|
||||
const data = JSON.parse(fs.readFileSync(tasksPath, 'utf8'));
|
||||
if (!data || !data.tasks || data.tasks.length === 0) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'INPUT_VALIDATION_ERROR',
|
||||
message: 'No valid tasks found in the tasks file'
|
||||
}
|
||||
};
|
||||
}
|
||||
taskIds = data.tasks.map(t => t.id).join(',');
|
||||
} else {
|
||||
// Use the provided task IDs
|
||||
taskIds = args.id;
|
||||
}
|
||||
|
||||
log.info(`Clearing subtasks from tasks: ${taskIds}`);
|
||||
|
||||
// Enable silent mode to prevent console logs from interfering with JSON response
|
||||
enableSilentMode();
|
||||
|
||||
// Call the core function
|
||||
clearSubtasks(tasksPath, taskIds);
|
||||
|
||||
// Restore normal logging
|
||||
disableSilentMode();
|
||||
|
||||
// Read the updated data to provide a summary
|
||||
const updatedData = JSON.parse(fs.readFileSync(tasksPath, 'utf8'));
|
||||
const taskIdArray = taskIds.split(',').map(id => parseInt(id.trim(), 10));
|
||||
|
||||
// Build a summary of what was done
|
||||
const clearedTasksCount = taskIdArray.length;
|
||||
const taskSummary = taskIdArray.map(id => {
|
||||
const task = updatedData.tasks.find(t => t.id === id);
|
||||
return task ? { id, title: task.title } : { id, title: 'Task not found' };
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
message: `Successfully cleared subtasks from ${clearedTasksCount} task(s)`,
|
||||
tasksCleared: taskSummary
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
// Make sure to restore normal logging even if there's an error
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Error in clearSubtasksDirect: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'CORE_FUNCTION_ERROR',
|
||||
message: error.message
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
// Either id or all must be provided
|
||||
if (!args.id && !args.all) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'INPUT_VALIDATION_ERROR',
|
||||
message:
|
||||
'Either task IDs with id parameter or all parameter must be provided'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Find the tasks.json path
|
||||
const tasksPath = findTasksJsonPath(args, log);
|
||||
|
||||
// Check if tasks.json exists
|
||||
if (!fs.existsSync(tasksPath)) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'FILE_NOT_FOUND_ERROR',
|
||||
message: `Tasks file not found at ${tasksPath}`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let taskIds;
|
||||
|
||||
// If all is specified, get all task IDs
|
||||
if (args.all) {
|
||||
log.info('Clearing subtasks from all tasks');
|
||||
const data = JSON.parse(fs.readFileSync(tasksPath, 'utf8'));
|
||||
if (!data || !data.tasks || data.tasks.length === 0) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'INPUT_VALIDATION_ERROR',
|
||||
message: 'No valid tasks found in the tasks file'
|
||||
}
|
||||
};
|
||||
}
|
||||
taskIds = data.tasks.map((t) => t.id).join(',');
|
||||
} else {
|
||||
// Use the provided task IDs
|
||||
taskIds = args.id;
|
||||
}
|
||||
|
||||
log.info(`Clearing subtasks from tasks: ${taskIds}`);
|
||||
|
||||
// Enable silent mode to prevent console logs from interfering with JSON response
|
||||
enableSilentMode();
|
||||
|
||||
// Call the core function
|
||||
clearSubtasks(tasksPath, taskIds);
|
||||
|
||||
// Restore normal logging
|
||||
disableSilentMode();
|
||||
|
||||
// Read the updated data to provide a summary
|
||||
const updatedData = JSON.parse(fs.readFileSync(tasksPath, 'utf8'));
|
||||
const taskIdArray = taskIds.split(',').map((id) => parseInt(id.trim(), 10));
|
||||
|
||||
// Build a summary of what was done
|
||||
const clearedTasksCount = taskIdArray.length;
|
||||
const taskSummary = taskIdArray.map((id) => {
|
||||
const task = updatedData.tasks.find((t) => t.id === id);
|
||||
return task ? { id, title: task.title } : { id, title: 'Task not found' };
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
message: `Successfully cleared subtasks from ${clearedTasksCount} task(s)`,
|
||||
tasksCleared: taskSummary
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
// Make sure to restore normal logging even if there's an error
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Error in clearSubtasksDirect: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'CORE_FUNCTION_ERROR',
|
||||
message: error.message
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,119 +3,131 @@
|
||||
* Direct function implementation for displaying complexity analysis report
|
||||
*/
|
||||
|
||||
import { readComplexityReport, enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js';
|
||||
import {
|
||||
readComplexityReport,
|
||||
enableSilentMode,
|
||||
disableSilentMode
|
||||
} from '../../../../scripts/modules/utils.js';
|
||||
import { findTasksJsonPath } from '../utils/path-utils.js';
|
||||
import { getCachedOrExecute } from '../../tools/utils.js';
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* Direct function wrapper for displaying the complexity report with error handling and caching.
|
||||
*
|
||||
*
|
||||
* @param {Object} args - Command arguments containing file path option
|
||||
* @param {Object} log - Logger object
|
||||
* @returns {Promise<Object>} - Result object with success status and data/error information
|
||||
*/
|
||||
export async function complexityReportDirect(args, log) {
|
||||
try {
|
||||
log.info(`Getting complexity report with args: ${JSON.stringify(args)}`);
|
||||
|
||||
// Get tasks file path to determine project root for the default report location
|
||||
let tasksPath;
|
||||
try {
|
||||
tasksPath = findTasksJsonPath(args, log);
|
||||
} catch (error) {
|
||||
log.warn(`Tasks file not found, using current directory: ${error.message}`);
|
||||
// Continue with default or specified report path
|
||||
}
|
||||
try {
|
||||
log.info(`Getting complexity report with args: ${JSON.stringify(args)}`);
|
||||
|
||||
// Get report file path from args or use default
|
||||
const reportPath = args.file || path.join(process.cwd(), 'scripts', 'task-complexity-report.json');
|
||||
|
||||
log.info(`Looking for complexity report at: ${reportPath}`);
|
||||
|
||||
// Generate cache key based on report path
|
||||
const cacheKey = `complexityReport:${reportPath}`;
|
||||
|
||||
// Define the core action function to read the report
|
||||
const coreActionFn = async () => {
|
||||
try {
|
||||
// Enable silent mode to prevent console logs from interfering with JSON response
|
||||
enableSilentMode();
|
||||
|
||||
const report = readComplexityReport(reportPath);
|
||||
|
||||
// Restore normal logging
|
||||
disableSilentMode();
|
||||
|
||||
if (!report) {
|
||||
log.warn(`No complexity report found at ${reportPath}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'FILE_NOT_FOUND_ERROR',
|
||||
message: `No complexity report found at ${reportPath}. Run 'analyze-complexity' first.`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
report,
|
||||
reportPath
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
// Make sure to restore normal logging even if there's an error
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Error reading complexity report: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'READ_ERROR',
|
||||
message: error.message
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
// Get tasks file path to determine project root for the default report location
|
||||
let tasksPath;
|
||||
try {
|
||||
tasksPath = findTasksJsonPath(args, log);
|
||||
} catch (error) {
|
||||
log.warn(
|
||||
`Tasks file not found, using current directory: ${error.message}`
|
||||
);
|
||||
// Continue with default or specified report path
|
||||
}
|
||||
|
||||
// Use the caching utility
|
||||
try {
|
||||
const result = await getCachedOrExecute({
|
||||
cacheKey,
|
||||
actionFn: coreActionFn,
|
||||
log
|
||||
});
|
||||
log.info(`complexityReportDirect completed. From cache: ${result.fromCache}`);
|
||||
return result; // Returns { success, data/error, fromCache }
|
||||
} catch (error) {
|
||||
// Catch unexpected errors from getCachedOrExecute itself
|
||||
// Ensure silent mode is disabled
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Unexpected error during getCachedOrExecute for complexityReport: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'UNEXPECTED_ERROR',
|
||||
message: error.message
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
// Ensure silent mode is disabled if an outer error occurs
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Error in complexityReportDirect: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'UNEXPECTED_ERROR',
|
||||
message: error.message
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
}
|
||||
// Get report file path from args or use default
|
||||
const reportPath =
|
||||
args.file ||
|
||||
path.join(process.cwd(), 'scripts', 'task-complexity-report.json');
|
||||
|
||||
log.info(`Looking for complexity report at: ${reportPath}`);
|
||||
|
||||
// Generate cache key based on report path
|
||||
const cacheKey = `complexityReport:${reportPath}`;
|
||||
|
||||
// Define the core action function to read the report
|
||||
const coreActionFn = async () => {
|
||||
try {
|
||||
// Enable silent mode to prevent console logs from interfering with JSON response
|
||||
enableSilentMode();
|
||||
|
||||
const report = readComplexityReport(reportPath);
|
||||
|
||||
// Restore normal logging
|
||||
disableSilentMode();
|
||||
|
||||
if (!report) {
|
||||
log.warn(`No complexity report found at ${reportPath}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'FILE_NOT_FOUND_ERROR',
|
||||
message: `No complexity report found at ${reportPath}. Run 'analyze-complexity' first.`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
report,
|
||||
reportPath
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
// Make sure to restore normal logging even if there's an error
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Error reading complexity report: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'READ_ERROR',
|
||||
message: error.message
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Use the caching utility
|
||||
try {
|
||||
const result = await getCachedOrExecute({
|
||||
cacheKey,
|
||||
actionFn: coreActionFn,
|
||||
log
|
||||
});
|
||||
log.info(
|
||||
`complexityReportDirect completed. From cache: ${result.fromCache}`
|
||||
);
|
||||
return result; // Returns { success, data/error, fromCache }
|
||||
} catch (error) {
|
||||
// Catch unexpected errors from getCachedOrExecute itself
|
||||
// Ensure silent mode is disabled
|
||||
disableSilentMode();
|
||||
|
||||
log.error(
|
||||
`Unexpected error during getCachedOrExecute for complexityReport: ${error.message}`
|
||||
);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'UNEXPECTED_ERROR',
|
||||
message: error.message
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
// Ensure silent mode is disabled if an outer error occurs
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Error in complexityReportDirect: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'UNEXPECTED_ERROR',
|
||||
message: error.message
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
*/
|
||||
|
||||
import { expandAllTasks } from '../../../../scripts/modules/task-manager.js';
|
||||
import { enableSilentMode, disableSilentMode, isSilentMode } from '../../../../scripts/modules/utils.js';
|
||||
import {
|
||||
enableSilentMode,
|
||||
disableSilentMode,
|
||||
isSilentMode
|
||||
} from '../../../../scripts/modules/utils.js';
|
||||
import { findTasksJsonPath } from '../utils/path-utils.js';
|
||||
import { getAnthropicClientForMCP } from '../utils/ai-client-utils.js';
|
||||
import path from 'path';
|
||||
@@ -23,98 +27,100 @@ import fs from 'fs';
|
||||
* @returns {Promise<{success: boolean, data?: Object, error?: {code: string, message: string}}>}
|
||||
*/
|
||||
export async function expandAllTasksDirect(args, log, context = {}) {
|
||||
const { session } = context; // Only extract session, not reportProgress
|
||||
|
||||
try {
|
||||
log.info(`Expanding all tasks with args: ${JSON.stringify(args)}`);
|
||||
|
||||
// Enable silent mode early to prevent any console output
|
||||
enableSilentMode();
|
||||
|
||||
try {
|
||||
// Find the tasks.json path
|
||||
const tasksPath = findTasksJsonPath(args, log);
|
||||
|
||||
// Parse parameters
|
||||
const numSubtasks = args.num ? parseInt(args.num, 10) : undefined;
|
||||
const useResearch = args.research === true;
|
||||
const additionalContext = args.prompt || '';
|
||||
const forceFlag = args.force === true;
|
||||
|
||||
log.info(`Expanding all tasks with ${numSubtasks || 'default'} subtasks each...`);
|
||||
|
||||
if (useResearch) {
|
||||
log.info('Using Perplexity AI for research-backed subtask generation');
|
||||
|
||||
// Initialize AI client for research-backed expansion
|
||||
try {
|
||||
await getAnthropicClientForMCP(session, log);
|
||||
} catch (error) {
|
||||
// Ensure silent mode is disabled before returning error
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Failed to initialize AI client: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'AI_CLIENT_ERROR',
|
||||
message: `Cannot initialize AI client: ${error.message}`
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (additionalContext) {
|
||||
log.info(`Additional context: "${additionalContext}"`);
|
||||
}
|
||||
if (forceFlag) {
|
||||
log.info('Force regeneration of subtasks is enabled');
|
||||
}
|
||||
|
||||
// Call the core function with session context for AI operations
|
||||
// and outputFormat as 'json' to prevent UI elements
|
||||
const result = await expandAllTasks(
|
||||
tasksPath,
|
||||
numSubtasks,
|
||||
useResearch,
|
||||
additionalContext,
|
||||
forceFlag,
|
||||
{ mcpLog: log, session },
|
||||
'json' // Use JSON output format to prevent UI elements
|
||||
);
|
||||
|
||||
// The expandAllTasks function now returns a result object
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
message: "Successfully expanded all pending tasks with subtasks",
|
||||
details: {
|
||||
numSubtasks: numSubtasks,
|
||||
research: useResearch,
|
||||
prompt: additionalContext,
|
||||
force: forceFlag,
|
||||
tasksExpanded: result.expandedCount,
|
||||
totalEligibleTasks: result.tasksToExpand
|
||||
}
|
||||
}
|
||||
};
|
||||
} finally {
|
||||
// Restore normal logging in finally block to ensure it runs even if there's an error
|
||||
disableSilentMode();
|
||||
}
|
||||
} catch (error) {
|
||||
// Ensure silent mode is disabled if an error occurs
|
||||
if (isSilentMode()) {
|
||||
disableSilentMode();
|
||||
}
|
||||
|
||||
log.error(`Error in expandAllTasksDirect: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'CORE_FUNCTION_ERROR',
|
||||
message: error.message
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
const { session } = context; // Only extract session, not reportProgress
|
||||
|
||||
try {
|
||||
log.info(`Expanding all tasks with args: ${JSON.stringify(args)}`);
|
||||
|
||||
// Enable silent mode early to prevent any console output
|
||||
enableSilentMode();
|
||||
|
||||
try {
|
||||
// Find the tasks.json path
|
||||
const tasksPath = findTasksJsonPath(args, log);
|
||||
|
||||
// Parse parameters
|
||||
const numSubtasks = args.num ? parseInt(args.num, 10) : undefined;
|
||||
const useResearch = args.research === true;
|
||||
const additionalContext = args.prompt || '';
|
||||
const forceFlag = args.force === true;
|
||||
|
||||
log.info(
|
||||
`Expanding all tasks with ${numSubtasks || 'default'} subtasks each...`
|
||||
);
|
||||
|
||||
if (useResearch) {
|
||||
log.info('Using Perplexity AI for research-backed subtask generation');
|
||||
|
||||
// Initialize AI client for research-backed expansion
|
||||
try {
|
||||
await getAnthropicClientForMCP(session, log);
|
||||
} catch (error) {
|
||||
// Ensure silent mode is disabled before returning error
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Failed to initialize AI client: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'AI_CLIENT_ERROR',
|
||||
message: `Cannot initialize AI client: ${error.message}`
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (additionalContext) {
|
||||
log.info(`Additional context: "${additionalContext}"`);
|
||||
}
|
||||
if (forceFlag) {
|
||||
log.info('Force regeneration of subtasks is enabled');
|
||||
}
|
||||
|
||||
// Call the core function with session context for AI operations
|
||||
// and outputFormat as 'json' to prevent UI elements
|
||||
const result = await expandAllTasks(
|
||||
tasksPath,
|
||||
numSubtasks,
|
||||
useResearch,
|
||||
additionalContext,
|
||||
forceFlag,
|
||||
{ mcpLog: log, session },
|
||||
'json' // Use JSON output format to prevent UI elements
|
||||
);
|
||||
|
||||
// The expandAllTasks function now returns a result object
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
message: 'Successfully expanded all pending tasks with subtasks',
|
||||
details: {
|
||||
numSubtasks: numSubtasks,
|
||||
research: useResearch,
|
||||
prompt: additionalContext,
|
||||
force: forceFlag,
|
||||
tasksExpanded: result.expandedCount,
|
||||
totalEligibleTasks: result.tasksToExpand
|
||||
}
|
||||
}
|
||||
};
|
||||
} finally {
|
||||
// Restore normal logging in finally block to ensure it runs even if there's an error
|
||||
disableSilentMode();
|
||||
}
|
||||
} catch (error) {
|
||||
// Ensure silent mode is disabled if an error occurs
|
||||
if (isSilentMode()) {
|
||||
disableSilentMode();
|
||||
}
|
||||
|
||||
log.error(`Error in expandAllTasksDirect: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'CORE_FUNCTION_ERROR',
|
||||
message: error.message
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,18 @@
|
||||
*/
|
||||
|
||||
import { expandTask } from '../../../../scripts/modules/task-manager.js';
|
||||
import { readJSON, writeJSON, enableSilentMode, disableSilentMode, isSilentMode } from '../../../../scripts/modules/utils.js';
|
||||
import {
|
||||
readJSON,
|
||||
writeJSON,
|
||||
enableSilentMode,
|
||||
disableSilentMode,
|
||||
isSilentMode
|
||||
} from '../../../../scripts/modules/utils.js';
|
||||
import { findTasksJsonPath } from '../utils/path-utils.js';
|
||||
import { getAnthropicClientForMCP, getModelConfig } from '../utils/ai-client-utils.js';
|
||||
import {
|
||||
getAnthropicClientForMCP,
|
||||
getModelConfig
|
||||
} from '../utils/ai-client-utils.js';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
@@ -19,231 +28,248 @@ import fs from 'fs';
|
||||
* @returns {Promise<Object>} - Task expansion result { success: boolean, data?: any, error?: { code: string, message: string }, fromCache: boolean }
|
||||
*/
|
||||
export async function expandTaskDirect(args, log, context = {}) {
|
||||
const { session } = context;
|
||||
|
||||
// Log session root data for debugging
|
||||
log.info(`Session data in expandTaskDirect: ${JSON.stringify({
|
||||
hasSession: !!session,
|
||||
sessionKeys: session ? Object.keys(session) : [],
|
||||
roots: session?.roots,
|
||||
rootsStr: JSON.stringify(session?.roots)
|
||||
})}`);
|
||||
|
||||
let tasksPath;
|
||||
try {
|
||||
// If a direct file path is provided, use it directly
|
||||
if (args.file && fs.existsSync(args.file)) {
|
||||
log.info(`[expandTaskDirect] Using explicitly provided tasks file: ${args.file}`);
|
||||
tasksPath = args.file;
|
||||
} else {
|
||||
// Find the tasks path through standard logic
|
||||
log.info(`[expandTaskDirect] No direct file path provided or file not found at ${args.file}, searching using findTasksJsonPath`);
|
||||
tasksPath = findTasksJsonPath(args, log);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`[expandTaskDirect] Error during tasksPath determination: ${error.message}`);
|
||||
|
||||
// Include session roots information in error
|
||||
const sessionRootsInfo = session ?
|
||||
`\nSession.roots: ${JSON.stringify(session.roots)}\n` +
|
||||
`Current Working Directory: ${process.cwd()}\n` +
|
||||
`Args.projectRoot: ${args.projectRoot}\n` +
|
||||
`Args.file: ${args.file}\n` :
|
||||
'\nSession object not available';
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'FILE_NOT_FOUND_ERROR',
|
||||
message: `Error determining tasksPath: ${error.message}${sessionRootsInfo}`
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
const { session } = context;
|
||||
|
||||
log.info(`[expandTaskDirect] Determined tasksPath: ${tasksPath}`);
|
||||
// Log session root data for debugging
|
||||
log.info(
|
||||
`Session data in expandTaskDirect: ${JSON.stringify({
|
||||
hasSession: !!session,
|
||||
sessionKeys: session ? Object.keys(session) : [],
|
||||
roots: session?.roots,
|
||||
rootsStr: JSON.stringify(session?.roots)
|
||||
})}`
|
||||
);
|
||||
|
||||
// Validate task ID
|
||||
const taskId = args.id ? parseInt(args.id, 10) : null;
|
||||
if (!taskId) {
|
||||
log.error('Task ID is required');
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'INPUT_VALIDATION_ERROR',
|
||||
message: 'Task ID is required'
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
let tasksPath;
|
||||
try {
|
||||
// If a direct file path is provided, use it directly
|
||||
if (args.file && fs.existsSync(args.file)) {
|
||||
log.info(
|
||||
`[expandTaskDirect] Using explicitly provided tasks file: ${args.file}`
|
||||
);
|
||||
tasksPath = args.file;
|
||||
} else {
|
||||
// Find the tasks path through standard logic
|
||||
log.info(
|
||||
`[expandTaskDirect] No direct file path provided or file not found at ${args.file}, searching using findTasksJsonPath`
|
||||
);
|
||||
tasksPath = findTasksJsonPath(args, log);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(
|
||||
`[expandTaskDirect] Error during tasksPath determination: ${error.message}`
|
||||
);
|
||||
|
||||
// Process other parameters
|
||||
const numSubtasks = args.num ? parseInt(args.num, 10) : undefined;
|
||||
const useResearch = args.research === true;
|
||||
const additionalContext = args.prompt || '';
|
||||
// Include session roots information in error
|
||||
const sessionRootsInfo = session
|
||||
? `\nSession.roots: ${JSON.stringify(session.roots)}\n` +
|
||||
`Current Working Directory: ${process.cwd()}\n` +
|
||||
`Args.projectRoot: ${args.projectRoot}\n` +
|
||||
`Args.file: ${args.file}\n`
|
||||
: '\nSession object not available';
|
||||
|
||||
// Initialize AI client if needed (for expandTask function)
|
||||
try {
|
||||
// This ensures the AI client is available by checking it
|
||||
if (useResearch) {
|
||||
log.info('Verifying AI client for research-backed expansion');
|
||||
await getAnthropicClientForMCP(session, log);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`Failed to initialize AI client: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'AI_CLIENT_ERROR',
|
||||
message: `Cannot initialize AI client: ${error.message}`
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'FILE_NOT_FOUND_ERROR',
|
||||
message: `Error determining tasksPath: ${error.message}${sessionRootsInfo}`
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
log.info(`[expandTaskDirect] Expanding task ${taskId} into ${numSubtasks || 'default'} subtasks. Research: ${useResearch}`);
|
||||
|
||||
// Read tasks data
|
||||
log.info(`[expandTaskDirect] Attempting to read JSON from: ${tasksPath}`);
|
||||
const data = readJSON(tasksPath);
|
||||
log.info(`[expandTaskDirect] Result of readJSON: ${data ? 'Data read successfully' : 'readJSON returned null or undefined'}`);
|
||||
log.info(`[expandTaskDirect] Determined tasksPath: ${tasksPath}`);
|
||||
|
||||
if (!data || !data.tasks) {
|
||||
log.error(`[expandTaskDirect] readJSON failed or returned invalid data for path: ${tasksPath}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'INVALID_TASKS_FILE',
|
||||
message: `No valid tasks found in ${tasksPath}. readJSON returned: ${JSON.stringify(data)}`
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Find the specific task
|
||||
log.info(`[expandTaskDirect] Searching for task ID ${taskId} in data`);
|
||||
const task = data.tasks.find(t => t.id === taskId);
|
||||
log.info(`[expandTaskDirect] Task found: ${task ? 'Yes' : 'No'}`);
|
||||
|
||||
if (!task) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'TASK_NOT_FOUND',
|
||||
message: `Task with ID ${taskId} not found`
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Check if task is completed
|
||||
if (task.status === 'done' || task.status === 'completed') {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'TASK_COMPLETED',
|
||||
message: `Task ${taskId} is already marked as ${task.status} and cannot be expanded`
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Check for existing subtasks
|
||||
const hasExistingSubtasks = task.subtasks && task.subtasks.length > 0;
|
||||
|
||||
// If the task already has subtasks, just return it (matching core behavior)
|
||||
if (hasExistingSubtasks) {
|
||||
log.info(`Task ${taskId} already has ${task.subtasks.length} subtasks`);
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
task,
|
||||
subtasksAdded: 0,
|
||||
hasExistingSubtasks
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Keep a copy of the task before modification
|
||||
const originalTask = JSON.parse(JSON.stringify(task));
|
||||
|
||||
// Tracking subtasks count before expansion
|
||||
const subtasksCountBefore = task.subtasks ? task.subtasks.length : 0;
|
||||
|
||||
// Create a backup of the tasks.json file
|
||||
const backupPath = path.join(path.dirname(tasksPath), 'tasks.json.bak');
|
||||
fs.copyFileSync(tasksPath, backupPath);
|
||||
|
||||
// Directly modify the data instead of calling the CLI function
|
||||
if (!task.subtasks) {
|
||||
task.subtasks = [];
|
||||
}
|
||||
|
||||
// Save tasks.json with potentially empty subtasks array
|
||||
writeJSON(tasksPath, data);
|
||||
|
||||
// Process the request
|
||||
try {
|
||||
// Enable silent mode to prevent console logs from interfering with JSON response
|
||||
enableSilentMode();
|
||||
|
||||
// Call expandTask with session context to ensure AI client is properly initialized
|
||||
const result = await expandTask(
|
||||
tasksPath,
|
||||
taskId,
|
||||
numSubtasks,
|
||||
useResearch,
|
||||
additionalContext,
|
||||
{ mcpLog: log, session } // Only pass mcpLog and session, NOT reportProgress
|
||||
);
|
||||
|
||||
// Restore normal logging
|
||||
disableSilentMode();
|
||||
|
||||
// Read the updated data
|
||||
const updatedData = readJSON(tasksPath);
|
||||
const updatedTask = updatedData.tasks.find(t => t.id === taskId);
|
||||
|
||||
// Calculate how many subtasks were added
|
||||
const subtasksAdded = updatedTask.subtasks ?
|
||||
updatedTask.subtasks.length - subtasksCountBefore : 0;
|
||||
|
||||
// Return the result
|
||||
log.info(`Successfully expanded task ${taskId} with ${subtasksAdded} new subtasks`);
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
task: updatedTask,
|
||||
subtasksAdded,
|
||||
hasExistingSubtasks
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
} catch (error) {
|
||||
// Make sure to restore normal logging even if there's an error
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Error expanding task: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'CORE_FUNCTION_ERROR',
|
||||
message: error.message || 'Failed to expand task'
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`Error expanding task: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'CORE_FUNCTION_ERROR',
|
||||
message: error.message || 'Failed to expand task'
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
}
|
||||
// Validate task ID
|
||||
const taskId = args.id ? parseInt(args.id, 10) : null;
|
||||
if (!taskId) {
|
||||
log.error('Task ID is required');
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'INPUT_VALIDATION_ERROR',
|
||||
message: 'Task ID is required'
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Process other parameters
|
||||
const numSubtasks = args.num ? parseInt(args.num, 10) : undefined;
|
||||
const useResearch = args.research === true;
|
||||
const additionalContext = args.prompt || '';
|
||||
|
||||
// Initialize AI client if needed (for expandTask function)
|
||||
try {
|
||||
// This ensures the AI client is available by checking it
|
||||
if (useResearch) {
|
||||
log.info('Verifying AI client for research-backed expansion');
|
||||
await getAnthropicClientForMCP(session, log);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`Failed to initialize AI client: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'AI_CLIENT_ERROR',
|
||||
message: `Cannot initialize AI client: ${error.message}`
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
log.info(
|
||||
`[expandTaskDirect] Expanding task ${taskId} into ${numSubtasks || 'default'} subtasks. Research: ${useResearch}`
|
||||
);
|
||||
|
||||
// Read tasks data
|
||||
log.info(`[expandTaskDirect] Attempting to read JSON from: ${tasksPath}`);
|
||||
const data = readJSON(tasksPath);
|
||||
log.info(
|
||||
`[expandTaskDirect] Result of readJSON: ${data ? 'Data read successfully' : 'readJSON returned null or undefined'}`
|
||||
);
|
||||
|
||||
if (!data || !data.tasks) {
|
||||
log.error(
|
||||
`[expandTaskDirect] readJSON failed or returned invalid data for path: ${tasksPath}`
|
||||
);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'INVALID_TASKS_FILE',
|
||||
message: `No valid tasks found in ${tasksPath}. readJSON returned: ${JSON.stringify(data)}`
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Find the specific task
|
||||
log.info(`[expandTaskDirect] Searching for task ID ${taskId} in data`);
|
||||
const task = data.tasks.find((t) => t.id === taskId);
|
||||
log.info(`[expandTaskDirect] Task found: ${task ? 'Yes' : 'No'}`);
|
||||
|
||||
if (!task) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'TASK_NOT_FOUND',
|
||||
message: `Task with ID ${taskId} not found`
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Check if task is completed
|
||||
if (task.status === 'done' || task.status === 'completed') {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'TASK_COMPLETED',
|
||||
message: `Task ${taskId} is already marked as ${task.status} and cannot be expanded`
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Check for existing subtasks
|
||||
const hasExistingSubtasks = task.subtasks && task.subtasks.length > 0;
|
||||
|
||||
// If the task already has subtasks, just return it (matching core behavior)
|
||||
if (hasExistingSubtasks) {
|
||||
log.info(`Task ${taskId} already has ${task.subtasks.length} subtasks`);
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
task,
|
||||
subtasksAdded: 0,
|
||||
hasExistingSubtasks
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Keep a copy of the task before modification
|
||||
const originalTask = JSON.parse(JSON.stringify(task));
|
||||
|
||||
// Tracking subtasks count before expansion
|
||||
const subtasksCountBefore = task.subtasks ? task.subtasks.length : 0;
|
||||
|
||||
// Create a backup of the tasks.json file
|
||||
const backupPath = path.join(path.dirname(tasksPath), 'tasks.json.bak');
|
||||
fs.copyFileSync(tasksPath, backupPath);
|
||||
|
||||
// Directly modify the data instead of calling the CLI function
|
||||
if (!task.subtasks) {
|
||||
task.subtasks = [];
|
||||
}
|
||||
|
||||
// Save tasks.json with potentially empty subtasks array
|
||||
writeJSON(tasksPath, data);
|
||||
|
||||
// Process the request
|
||||
try {
|
||||
// Enable silent mode to prevent console logs from interfering with JSON response
|
||||
enableSilentMode();
|
||||
|
||||
// Call expandTask with session context to ensure AI client is properly initialized
|
||||
const result = await expandTask(
|
||||
tasksPath,
|
||||
taskId,
|
||||
numSubtasks,
|
||||
useResearch,
|
||||
additionalContext,
|
||||
{ mcpLog: log, session } // Only pass mcpLog and session, NOT reportProgress
|
||||
);
|
||||
|
||||
// Restore normal logging
|
||||
disableSilentMode();
|
||||
|
||||
// Read the updated data
|
||||
const updatedData = readJSON(tasksPath);
|
||||
const updatedTask = updatedData.tasks.find((t) => t.id === taskId);
|
||||
|
||||
// Calculate how many subtasks were added
|
||||
const subtasksAdded = updatedTask.subtasks
|
||||
? updatedTask.subtasks.length - subtasksCountBefore
|
||||
: 0;
|
||||
|
||||
// Return the result
|
||||
log.info(
|
||||
`Successfully expanded task ${taskId} with ${subtasksAdded} new subtasks`
|
||||
);
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
task: updatedTask,
|
||||
subtasksAdded,
|
||||
hasExistingSubtasks
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
} catch (error) {
|
||||
// Make sure to restore normal logging even if there's an error
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Error expanding task: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'CORE_FUNCTION_ERROR',
|
||||
message: error.message || 'Failed to expand task'
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`Error expanding task: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'CORE_FUNCTION_ERROR',
|
||||
message: error.message || 'Failed to expand task'
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
|
||||
import { fixDependenciesCommand } from '../../../../scripts/modules/dependency-manager.js';
|
||||
import { findTasksJsonPath } from '../utils/path-utils.js';
|
||||
import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js';
|
||||
import {
|
||||
enableSilentMode,
|
||||
disableSilentMode
|
||||
} from '../../../../scripts/modules/utils.js';
|
||||
import fs from 'fs';
|
||||
|
||||
/**
|
||||
@@ -16,50 +19,50 @@ import fs from 'fs';
|
||||
* @returns {Promise<{success: boolean, data?: Object, error?: {code: string, message: string}}>}
|
||||
*/
|
||||
export async function fixDependenciesDirect(args, log) {
|
||||
try {
|
||||
log.info(`Fixing invalid dependencies in tasks...`);
|
||||
|
||||
// Find the tasks.json path
|
||||
const tasksPath = findTasksJsonPath(args, log);
|
||||
|
||||
// Verify the file exists
|
||||
if (!fs.existsSync(tasksPath)) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'FILE_NOT_FOUND',
|
||||
message: `Tasks file not found at ${tasksPath}`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Enable silent mode to prevent console logs from interfering with JSON response
|
||||
enableSilentMode();
|
||||
|
||||
// Call the original command function
|
||||
await fixDependenciesCommand(tasksPath);
|
||||
|
||||
// Restore normal logging
|
||||
disableSilentMode();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
message: 'Dependencies fixed successfully',
|
||||
tasksPath
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
// Make sure to restore normal logging even if there's an error
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Error fixing dependencies: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'FIX_DEPENDENCIES_ERROR',
|
||||
message: error.message
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
try {
|
||||
log.info(`Fixing invalid dependencies in tasks...`);
|
||||
|
||||
// Find the tasks.json path
|
||||
const tasksPath = findTasksJsonPath(args, log);
|
||||
|
||||
// Verify the file exists
|
||||
if (!fs.existsSync(tasksPath)) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'FILE_NOT_FOUND',
|
||||
message: `Tasks file not found at ${tasksPath}`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Enable silent mode to prevent console logs from interfering with JSON response
|
||||
enableSilentMode();
|
||||
|
||||
// Call the original command function
|
||||
await fixDependenciesCommand(tasksPath);
|
||||
|
||||
// Restore normal logging
|
||||
disableSilentMode();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
message: 'Dependencies fixed successfully',
|
||||
tasksPath
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
// Make sure to restore normal logging even if there's an error
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Error fixing dependencies: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'FIX_DEPENDENCIES_ERROR',
|
||||
message: error.message
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,84 +4,91 @@
|
||||
*/
|
||||
|
||||
import { generateTaskFiles } from '../../../../scripts/modules/task-manager.js';
|
||||
import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js';
|
||||
import {
|
||||
enableSilentMode,
|
||||
disableSilentMode
|
||||
} from '../../../../scripts/modules/utils.js';
|
||||
import { findTasksJsonPath } from '../utils/path-utils.js';
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* Direct function wrapper for generateTaskFiles with error handling.
|
||||
*
|
||||
*
|
||||
* @param {Object} args - Command arguments containing file and output path options.
|
||||
* @param {Object} log - Logger object.
|
||||
* @returns {Promise<Object>} - Result object with success status and data/error information.
|
||||
*/
|
||||
export async function generateTaskFilesDirect(args, log) {
|
||||
try {
|
||||
log.info(`Generating task files with args: ${JSON.stringify(args)}`);
|
||||
|
||||
// Get tasks file path
|
||||
let tasksPath;
|
||||
try {
|
||||
tasksPath = findTasksJsonPath(args, log);
|
||||
} catch (error) {
|
||||
log.error(`Error finding tasks file: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'TASKS_FILE_ERROR', message: error.message },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Get output directory (defaults to the same directory as the tasks file)
|
||||
let outputDir = args.output;
|
||||
if (!outputDir) {
|
||||
outputDir = path.dirname(tasksPath);
|
||||
}
|
||||
|
||||
log.info(`Generating task files from ${tasksPath} to ${outputDir}`);
|
||||
|
||||
// Execute core generateTaskFiles function in a separate try/catch
|
||||
try {
|
||||
// Enable silent mode to prevent logs from being written to stdout
|
||||
enableSilentMode();
|
||||
|
||||
// The function is synchronous despite being awaited elsewhere
|
||||
generateTaskFiles(tasksPath, outputDir);
|
||||
|
||||
// Restore normal logging after task generation
|
||||
disableSilentMode();
|
||||
} catch (genError) {
|
||||
// Make sure to restore normal logging even if there's an error
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Error in generateTaskFiles: ${genError.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'GENERATE_FILES_ERROR', message: genError.message },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Return success with file paths
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
message: `Successfully generated task files`,
|
||||
tasksPath,
|
||||
outputDir,
|
||||
taskFiles: 'Individual task files have been generated in the output directory'
|
||||
},
|
||||
fromCache: false // This operation always modifies state and should never be cached
|
||||
};
|
||||
} catch (error) {
|
||||
// Make sure to restore normal logging if an outer error occurs
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Error generating task files: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'GENERATE_TASKS_ERROR', message: error.message || 'Unknown error generating task files' },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
}
|
||||
try {
|
||||
log.info(`Generating task files with args: ${JSON.stringify(args)}`);
|
||||
|
||||
// Get tasks file path
|
||||
let tasksPath;
|
||||
try {
|
||||
tasksPath = findTasksJsonPath(args, log);
|
||||
} catch (error) {
|
||||
log.error(`Error finding tasks file: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'TASKS_FILE_ERROR', message: error.message },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Get output directory (defaults to the same directory as the tasks file)
|
||||
let outputDir = args.output;
|
||||
if (!outputDir) {
|
||||
outputDir = path.dirname(tasksPath);
|
||||
}
|
||||
|
||||
log.info(`Generating task files from ${tasksPath} to ${outputDir}`);
|
||||
|
||||
// Execute core generateTaskFiles function in a separate try/catch
|
||||
try {
|
||||
// Enable silent mode to prevent logs from being written to stdout
|
||||
enableSilentMode();
|
||||
|
||||
// The function is synchronous despite being awaited elsewhere
|
||||
generateTaskFiles(tasksPath, outputDir);
|
||||
|
||||
// Restore normal logging after task generation
|
||||
disableSilentMode();
|
||||
} catch (genError) {
|
||||
// Make sure to restore normal logging even if there's an error
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Error in generateTaskFiles: ${genError.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'GENERATE_FILES_ERROR', message: genError.message },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Return success with file paths
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
message: `Successfully generated task files`,
|
||||
tasksPath,
|
||||
outputDir,
|
||||
taskFiles:
|
||||
'Individual task files have been generated in the output directory'
|
||||
},
|
||||
fromCache: false // This operation always modifies state and should never be cached
|
||||
};
|
||||
} catch (error) {
|
||||
// Make sure to restore normal logging if an outer error occurs
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Error generating task files: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'GENERATE_TASKS_ERROR',
|
||||
message: error.message || 'Unknown error generating task files'
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,10 @@
|
||||
import { listTasks } from '../../../../scripts/modules/task-manager.js';
|
||||
import { getCachedOrExecute } from '../../tools/utils.js';
|
||||
import { findTasksJsonPath } from '../utils/path-utils.js';
|
||||
import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js';
|
||||
import {
|
||||
enableSilentMode,
|
||||
disableSilentMode
|
||||
} from '../../../../scripts/modules/utils.js';
|
||||
|
||||
/**
|
||||
* Direct function wrapper for listTasks with error handling and caching.
|
||||
@@ -16,68 +19,102 @@ import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules
|
||||
* @returns {Promise<Object>} - Task list result { success: boolean, data?: any, error?: { code: string, message: string }, fromCache: boolean }.
|
||||
*/
|
||||
export async function listTasksDirect(args, log) {
|
||||
let tasksPath;
|
||||
try {
|
||||
// Find the tasks path first - needed for cache key and execution
|
||||
tasksPath = findTasksJsonPath(args, log);
|
||||
} catch (error) {
|
||||
if (error.code === 'TASKS_FILE_NOT_FOUND') {
|
||||
log.error(`Tasks file not found: ${error.message}`);
|
||||
// Return the error structure expected by the calling tool/handler
|
||||
return { success: false, error: { code: error.code, message: error.message }, fromCache: false };
|
||||
}
|
||||
log.error(`Unexpected error finding tasks file: ${error.message}`);
|
||||
// Re-throw for outer catch or return structured error
|
||||
return { success: false, error: { code: 'FIND_TASKS_PATH_ERROR', message: error.message }, fromCache: false };
|
||||
}
|
||||
let tasksPath;
|
||||
try {
|
||||
// Find the tasks path first - needed for cache key and execution
|
||||
tasksPath = findTasksJsonPath(args, log);
|
||||
} catch (error) {
|
||||
if (error.code === 'TASKS_FILE_NOT_FOUND') {
|
||||
log.error(`Tasks file not found: ${error.message}`);
|
||||
// Return the error structure expected by the calling tool/handler
|
||||
return {
|
||||
success: false,
|
||||
error: { code: error.code, message: error.message },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
log.error(`Unexpected error finding tasks file: ${error.message}`);
|
||||
// Re-throw for outer catch or return structured error
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'FIND_TASKS_PATH_ERROR', message: error.message },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Generate cache key *after* finding tasksPath
|
||||
const statusFilter = args.status || 'all';
|
||||
const withSubtasks = args.withSubtasks || false;
|
||||
const cacheKey = `listTasks:${tasksPath}:${statusFilter}:${withSubtasks}`;
|
||||
|
||||
// Define the action function to be executed on cache miss
|
||||
const coreListTasksAction = async () => {
|
||||
try {
|
||||
// Enable silent mode to prevent console logs from interfering with JSON response
|
||||
enableSilentMode();
|
||||
|
||||
log.info(`Executing core listTasks function for path: ${tasksPath}, filter: ${statusFilter}, subtasks: ${withSubtasks}`);
|
||||
const resultData = listTasks(tasksPath, statusFilter, withSubtasks, 'json');
|
||||
// Generate cache key *after* finding tasksPath
|
||||
const statusFilter = args.status || 'all';
|
||||
const withSubtasks = args.withSubtasks || false;
|
||||
const cacheKey = `listTasks:${tasksPath}:${statusFilter}:${withSubtasks}`;
|
||||
|
||||
if (!resultData || !resultData.tasks) {
|
||||
log.error('Invalid or empty response from listTasks core function');
|
||||
return { success: false, error: { code: 'INVALID_CORE_RESPONSE', message: 'Invalid or empty response from listTasks core function' } };
|
||||
}
|
||||
log.info(`Core listTasks function retrieved ${resultData.tasks.length} tasks`);
|
||||
|
||||
// Restore normal logging
|
||||
disableSilentMode();
|
||||
|
||||
return { success: true, data: resultData };
|
||||
// Define the action function to be executed on cache miss
|
||||
const coreListTasksAction = async () => {
|
||||
try {
|
||||
// Enable silent mode to prevent console logs from interfering with JSON response
|
||||
enableSilentMode();
|
||||
|
||||
} catch (error) {
|
||||
// Make sure to restore normal logging even if there's an error
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Core listTasks function failed: ${error.message}`);
|
||||
return { success: false, error: { code: 'LIST_TASKS_CORE_ERROR', message: error.message || 'Failed to list tasks' } };
|
||||
}
|
||||
};
|
||||
log.info(
|
||||
`Executing core listTasks function for path: ${tasksPath}, filter: ${statusFilter}, subtasks: ${withSubtasks}`
|
||||
);
|
||||
const resultData = listTasks(
|
||||
tasksPath,
|
||||
statusFilter,
|
||||
withSubtasks,
|
||||
'json'
|
||||
);
|
||||
|
||||
// Use the caching utility
|
||||
try {
|
||||
const result = await getCachedOrExecute({
|
||||
cacheKey,
|
||||
actionFn: coreListTasksAction,
|
||||
log
|
||||
});
|
||||
log.info(`listTasksDirect completed. From cache: ${result.fromCache}`);
|
||||
return result; // Returns { success, data/error, fromCache }
|
||||
} catch(error) {
|
||||
// Catch unexpected errors from getCachedOrExecute itself (though unlikely)
|
||||
log.error(`Unexpected error during getCachedOrExecute for listTasks: ${error.message}`);
|
||||
console.error(error.stack);
|
||||
return { success: false, error: { code: 'CACHE_UTIL_ERROR', message: error.message }, fromCache: false };
|
||||
}
|
||||
}
|
||||
if (!resultData || !resultData.tasks) {
|
||||
log.error('Invalid or empty response from listTasks core function');
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'INVALID_CORE_RESPONSE',
|
||||
message: 'Invalid or empty response from listTasks core function'
|
||||
}
|
||||
};
|
||||
}
|
||||
log.info(
|
||||
`Core listTasks function retrieved ${resultData.tasks.length} tasks`
|
||||
);
|
||||
|
||||
// Restore normal logging
|
||||
disableSilentMode();
|
||||
|
||||
return { success: true, data: resultData };
|
||||
} catch (error) {
|
||||
// Make sure to restore normal logging even if there's an error
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Core listTasks function failed: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'LIST_TASKS_CORE_ERROR',
|
||||
message: error.message || 'Failed to list tasks'
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Use the caching utility
|
||||
try {
|
||||
const result = await getCachedOrExecute({
|
||||
cacheKey,
|
||||
actionFn: coreListTasksAction,
|
||||
log
|
||||
});
|
||||
log.info(`listTasksDirect completed. From cache: ${result.fromCache}`);
|
||||
return result; // Returns { success, data/error, fromCache }
|
||||
} catch (error) {
|
||||
// Catch unexpected errors from getCachedOrExecute itself (though unlikely)
|
||||
log.error(
|
||||
`Unexpected error during getCachedOrExecute for listTasks: ${error.message}`
|
||||
);
|
||||
console.error(error.stack);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'CACHE_UTIL_ERROR', message: error.message },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,10 @@ import { findNextTask } from '../../../../scripts/modules/task-manager.js';
|
||||
import { readJSON } from '../../../../scripts/modules/utils.js';
|
||||
import { getCachedOrExecute } from '../../tools/utils.js';
|
||||
import { findTasksJsonPath } from '../utils/path-utils.js';
|
||||
import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js';
|
||||
import {
|
||||
enableSilentMode,
|
||||
disableSilentMode
|
||||
} from '../../../../scripts/modules/utils.js';
|
||||
|
||||
/**
|
||||
* Direct function wrapper for finding the next task to work on with error handling and caching.
|
||||
@@ -17,106 +20,113 @@ import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules
|
||||
* @returns {Promise<Object>} - Next task result { success: boolean, data?: any, error?: { code: string, message: string }, fromCache: boolean }
|
||||
*/
|
||||
export async function nextTaskDirect(args, log) {
|
||||
let tasksPath;
|
||||
try {
|
||||
// Find the tasks path first - needed for cache key and execution
|
||||
tasksPath = findTasksJsonPath(args, log);
|
||||
} catch (error) {
|
||||
log.error(`Tasks file not found: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'FILE_NOT_FOUND_ERROR',
|
||||
message: error.message
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
let tasksPath;
|
||||
try {
|
||||
// Find the tasks path first - needed for cache key and execution
|
||||
tasksPath = findTasksJsonPath(args, log);
|
||||
} catch (error) {
|
||||
log.error(`Tasks file not found: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'FILE_NOT_FOUND_ERROR',
|
||||
message: error.message
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Generate cache key using task path
|
||||
const cacheKey = `nextTask:${tasksPath}`;
|
||||
|
||||
// Define the action function to be executed on cache miss
|
||||
const coreNextTaskAction = async () => {
|
||||
try {
|
||||
// Enable silent mode to prevent console logs from interfering with JSON response
|
||||
enableSilentMode();
|
||||
|
||||
log.info(`Finding next task from ${tasksPath}`);
|
||||
|
||||
// Read tasks data
|
||||
const data = readJSON(tasksPath);
|
||||
if (!data || !data.tasks) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'INVALID_TASKS_FILE',
|
||||
message: `No valid tasks found in ${tasksPath}`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Find the next task
|
||||
const nextTask = findNextTask(data.tasks);
|
||||
|
||||
if (!nextTask) {
|
||||
log.info('No eligible next task found. All tasks are either completed or have unsatisfied dependencies');
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
message: 'No eligible next task found. All tasks are either completed or have unsatisfied dependencies',
|
||||
nextTask: null,
|
||||
allTasks: data.tasks
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Restore normal logging
|
||||
disableSilentMode();
|
||||
|
||||
// Return the next task data with the full tasks array for reference
|
||||
log.info(`Successfully found next task ${nextTask.id}: ${nextTask.title}`);
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
nextTask,
|
||||
allTasks: data.tasks
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
// Make sure to restore normal logging even if there's an error
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Error finding next task: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'CORE_FUNCTION_ERROR',
|
||||
message: error.message || 'Failed to find next task'
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
// Generate cache key using task path
|
||||
const cacheKey = `nextTask:${tasksPath}`;
|
||||
|
||||
// Use the caching utility
|
||||
try {
|
||||
const result = await getCachedOrExecute({
|
||||
cacheKey,
|
||||
actionFn: coreNextTaskAction,
|
||||
log
|
||||
});
|
||||
log.info(`nextTaskDirect completed. From cache: ${result.fromCache}`);
|
||||
return result; // Returns { success, data/error, fromCache }
|
||||
} catch (error) {
|
||||
// Catch unexpected errors from getCachedOrExecute itself
|
||||
log.error(`Unexpected error during getCachedOrExecute for nextTask: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'UNEXPECTED_ERROR',
|
||||
message: error.message
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
}
|
||||
// Define the action function to be executed on cache miss
|
||||
const coreNextTaskAction = async () => {
|
||||
try {
|
||||
// Enable silent mode to prevent console logs from interfering with JSON response
|
||||
enableSilentMode();
|
||||
|
||||
log.info(`Finding next task from ${tasksPath}`);
|
||||
|
||||
// Read tasks data
|
||||
const data = readJSON(tasksPath);
|
||||
if (!data || !data.tasks) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'INVALID_TASKS_FILE',
|
||||
message: `No valid tasks found in ${tasksPath}`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Find the next task
|
||||
const nextTask = findNextTask(data.tasks);
|
||||
|
||||
if (!nextTask) {
|
||||
log.info(
|
||||
'No eligible next task found. All tasks are either completed or have unsatisfied dependencies'
|
||||
);
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
message:
|
||||
'No eligible next task found. All tasks are either completed or have unsatisfied dependencies',
|
||||
nextTask: null,
|
||||
allTasks: data.tasks
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Restore normal logging
|
||||
disableSilentMode();
|
||||
|
||||
// Return the next task data with the full tasks array for reference
|
||||
log.info(
|
||||
`Successfully found next task ${nextTask.id}: ${nextTask.title}`
|
||||
);
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
nextTask,
|
||||
allTasks: data.tasks
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
// Make sure to restore normal logging even if there's an error
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Error finding next task: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'CORE_FUNCTION_ERROR',
|
||||
message: error.message || 'Failed to find next task'
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Use the caching utility
|
||||
try {
|
||||
const result = await getCachedOrExecute({
|
||||
cacheKey,
|
||||
actionFn: coreNextTaskAction,
|
||||
log
|
||||
});
|
||||
log.info(`nextTaskDirect completed. From cache: ${result.fromCache}`);
|
||||
return result; // Returns { success, data/error, fromCache }
|
||||
} catch (error) {
|
||||
// Catch unexpected errors from getCachedOrExecute itself
|
||||
log.error(
|
||||
`Unexpected error during getCachedOrExecute for nextTask: ${error.message}`
|
||||
);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'UNEXPECTED_ERROR',
|
||||
message: error.message
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,144 +7,172 @@ import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { parsePRD } from '../../../../scripts/modules/task-manager.js';
|
||||
import { findTasksJsonPath } from '../utils/path-utils.js';
|
||||
import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js';
|
||||
import { getAnthropicClientForMCP, getModelConfig } from '../utils/ai-client-utils.js';
|
||||
import {
|
||||
enableSilentMode,
|
||||
disableSilentMode
|
||||
} from '../../../../scripts/modules/utils.js';
|
||||
import {
|
||||
getAnthropicClientForMCP,
|
||||
getModelConfig
|
||||
} from '../utils/ai-client-utils.js';
|
||||
|
||||
/**
|
||||
* Direct function wrapper for parsing PRD documents and generating tasks.
|
||||
*
|
||||
*
|
||||
* @param {Object} args - Command arguments containing input, numTasks or tasks, and output options.
|
||||
* @param {Object} log - Logger object.
|
||||
* @param {Object} context - Context object containing session data.
|
||||
* @returns {Promise<Object>} - Result object with success status and data/error information.
|
||||
*/
|
||||
export async function parsePRDDirect(args, log, context = {}) {
|
||||
const { session } = context; // Only extract session, not reportProgress
|
||||
|
||||
try {
|
||||
log.info(`Parsing PRD document with args: ${JSON.stringify(args)}`);
|
||||
|
||||
// Initialize AI client for PRD parsing
|
||||
let aiClient;
|
||||
try {
|
||||
aiClient = getAnthropicClientForMCP(session, log);
|
||||
} catch (error) {
|
||||
log.error(`Failed to initialize AI client: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'AI_CLIENT_ERROR',
|
||||
message: `Cannot initialize AI client: ${error.message}`
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Parameter validation and path resolution
|
||||
if (!args.input) {
|
||||
const errorMessage = 'No input file specified. Please provide an input PRD document path.';
|
||||
log.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'MISSING_INPUT_FILE', message: errorMessage },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Resolve input path (relative to project root if provided)
|
||||
const projectRoot = args.projectRoot || process.cwd();
|
||||
const inputPath = path.isAbsolute(args.input) ? args.input : path.resolve(projectRoot, args.input);
|
||||
|
||||
// Determine output path
|
||||
let outputPath;
|
||||
if (args.output) {
|
||||
outputPath = path.isAbsolute(args.output) ? args.output : path.resolve(projectRoot, args.output);
|
||||
} else {
|
||||
// Default to tasks/tasks.json in the project root
|
||||
outputPath = path.resolve(projectRoot, 'tasks', 'tasks.json');
|
||||
}
|
||||
|
||||
// Verify input file exists
|
||||
if (!fs.existsSync(inputPath)) {
|
||||
const errorMessage = `Input file not found: ${inputPath}`;
|
||||
log.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'INPUT_FILE_NOT_FOUND', message: errorMessage },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Parse number of tasks - handle both string and number values
|
||||
let numTasks = 10; // Default
|
||||
if (args.numTasks) {
|
||||
numTasks = typeof args.numTasks === 'string' ? parseInt(args.numTasks, 10) : args.numTasks;
|
||||
if (isNaN(numTasks)) {
|
||||
numTasks = 10; // Fallback to default if parsing fails
|
||||
log.warn(`Invalid numTasks value: ${args.numTasks}. Using default: 10`);
|
||||
}
|
||||
}
|
||||
|
||||
log.info(`Preparing to parse PRD from ${inputPath} and output to ${outputPath} with ${numTasks} tasks`);
|
||||
|
||||
// Create the logger wrapper for proper logging in the core function
|
||||
const logWrapper = {
|
||||
info: (message, ...args) => log.info(message, ...args),
|
||||
warn: (message, ...args) => log.warn(message, ...args),
|
||||
error: (message, ...args) => log.error(message, ...args),
|
||||
debug: (message, ...args) => log.debug && log.debug(message, ...args),
|
||||
success: (message, ...args) => log.info(message, ...args) // Map success to info
|
||||
};
|
||||
const { session } = context; // Only extract session, not reportProgress
|
||||
|
||||
// Get model config from session
|
||||
const modelConfig = getModelConfig(session);
|
||||
|
||||
// Enable silent mode to prevent console logs from interfering with JSON response
|
||||
enableSilentMode();
|
||||
try {
|
||||
// Execute core parsePRD function with AI client
|
||||
await parsePRD(inputPath, outputPath, numTasks, {
|
||||
mcpLog: logWrapper,
|
||||
session
|
||||
}, aiClient, modelConfig);
|
||||
|
||||
// Since parsePRD doesn't return a value but writes to a file, we'll read the result
|
||||
// to return it to the caller
|
||||
if (fs.existsSync(outputPath)) {
|
||||
const tasksData = JSON.parse(fs.readFileSync(outputPath, 'utf8'));
|
||||
log.info(`Successfully parsed PRD and generated ${tasksData.tasks?.length || 0} tasks`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
message: `Successfully generated ${tasksData.tasks?.length || 0} tasks from PRD`,
|
||||
taskCount: tasksData.tasks?.length || 0,
|
||||
outputPath
|
||||
},
|
||||
fromCache: false // This operation always modifies state and should never be cached
|
||||
};
|
||||
} else {
|
||||
const errorMessage = `Tasks file was not created at ${outputPath}`;
|
||||
log.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'OUTPUT_FILE_NOT_CREATED', message: errorMessage },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
} finally {
|
||||
// Always restore normal logging
|
||||
disableSilentMode();
|
||||
}
|
||||
} catch (error) {
|
||||
// Make sure to restore normal logging even if there's an error
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Error parsing PRD: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'PARSE_PRD_ERROR', message: error.message || 'Unknown error parsing PRD' },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
}
|
||||
try {
|
||||
log.info(`Parsing PRD document with args: ${JSON.stringify(args)}`);
|
||||
|
||||
// Initialize AI client for PRD parsing
|
||||
let aiClient;
|
||||
try {
|
||||
aiClient = getAnthropicClientForMCP(session, log);
|
||||
} catch (error) {
|
||||
log.error(`Failed to initialize AI client: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'AI_CLIENT_ERROR',
|
||||
message: `Cannot initialize AI client: ${error.message}`
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Parameter validation and path resolution
|
||||
if (!args.input) {
|
||||
const errorMessage =
|
||||
'No input file specified. Please provide an input PRD document path.';
|
||||
log.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'MISSING_INPUT_FILE', message: errorMessage },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Resolve input path (relative to project root if provided)
|
||||
const projectRoot = args.projectRoot || process.cwd();
|
||||
const inputPath = path.isAbsolute(args.input)
|
||||
? args.input
|
||||
: path.resolve(projectRoot, args.input);
|
||||
|
||||
// Determine output path
|
||||
let outputPath;
|
||||
if (args.output) {
|
||||
outputPath = path.isAbsolute(args.output)
|
||||
? args.output
|
||||
: path.resolve(projectRoot, args.output);
|
||||
} else {
|
||||
// Default to tasks/tasks.json in the project root
|
||||
outputPath = path.resolve(projectRoot, 'tasks', 'tasks.json');
|
||||
}
|
||||
|
||||
// Verify input file exists
|
||||
if (!fs.existsSync(inputPath)) {
|
||||
const errorMessage = `Input file not found: ${inputPath}`;
|
||||
log.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'INPUT_FILE_NOT_FOUND', message: errorMessage },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Parse number of tasks - handle both string and number values
|
||||
let numTasks = 10; // Default
|
||||
if (args.numTasks) {
|
||||
numTasks =
|
||||
typeof args.numTasks === 'string'
|
||||
? parseInt(args.numTasks, 10)
|
||||
: args.numTasks;
|
||||
if (isNaN(numTasks)) {
|
||||
numTasks = 10; // Fallback to default if parsing fails
|
||||
log.warn(`Invalid numTasks value: ${args.numTasks}. Using default: 10`);
|
||||
}
|
||||
}
|
||||
|
||||
log.info(
|
||||
`Preparing to parse PRD from ${inputPath} and output to ${outputPath} with ${numTasks} tasks`
|
||||
);
|
||||
|
||||
// Create the logger wrapper for proper logging in the core function
|
||||
const logWrapper = {
|
||||
info: (message, ...args) => log.info(message, ...args),
|
||||
warn: (message, ...args) => log.warn(message, ...args),
|
||||
error: (message, ...args) => log.error(message, ...args),
|
||||
debug: (message, ...args) => log.debug && log.debug(message, ...args),
|
||||
success: (message, ...args) => log.info(message, ...args) // Map success to info
|
||||
};
|
||||
|
||||
// Get model config from session
|
||||
const modelConfig = getModelConfig(session);
|
||||
|
||||
// Enable silent mode to prevent console logs from interfering with JSON response
|
||||
enableSilentMode();
|
||||
try {
|
||||
// Execute core parsePRD function with AI client
|
||||
await parsePRD(
|
||||
inputPath,
|
||||
outputPath,
|
||||
numTasks,
|
||||
{
|
||||
mcpLog: logWrapper,
|
||||
session
|
||||
},
|
||||
aiClient,
|
||||
modelConfig
|
||||
);
|
||||
|
||||
// Since parsePRD doesn't return a value but writes to a file, we'll read the result
|
||||
// to return it to the caller
|
||||
if (fs.existsSync(outputPath)) {
|
||||
const tasksData = JSON.parse(fs.readFileSync(outputPath, 'utf8'));
|
||||
log.info(
|
||||
`Successfully parsed PRD and generated ${tasksData.tasks?.length || 0} tasks`
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
message: `Successfully generated ${tasksData.tasks?.length || 0} tasks from PRD`,
|
||||
taskCount: tasksData.tasks?.length || 0,
|
||||
outputPath
|
||||
},
|
||||
fromCache: false // This operation always modifies state and should never be cached
|
||||
};
|
||||
} else {
|
||||
const errorMessage = `Tasks file was not created at ${outputPath}`;
|
||||
log.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'OUTPUT_FILE_NOT_CREATED', message: errorMessage },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
} finally {
|
||||
// Always restore normal logging
|
||||
disableSilentMode();
|
||||
}
|
||||
} catch (error) {
|
||||
// Make sure to restore normal logging even if there's an error
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Error parsing PRD: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'PARSE_PRD_ERROR',
|
||||
message: error.message || 'Unknown error parsing PRD'
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
|
||||
import { removeDependency } from '../../../../scripts/modules/dependency-manager.js';
|
||||
import { findTasksJsonPath } from '../utils/path-utils.js';
|
||||
import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js';
|
||||
import {
|
||||
enableSilentMode,
|
||||
disableSilentMode
|
||||
} from '../../../../scripts/modules/utils.js';
|
||||
|
||||
/**
|
||||
* Remove a dependency from a task
|
||||
@@ -17,67 +20,75 @@ import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules
|
||||
* @returns {Promise<{success: boolean, data?: Object, error?: {code: string, message: string}}>}
|
||||
*/
|
||||
export async function removeDependencyDirect(args, log) {
|
||||
try {
|
||||
log.info(`Removing dependency with args: ${JSON.stringify(args)}`);
|
||||
|
||||
// Validate required parameters
|
||||
if (!args.id) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'INPUT_VALIDATION_ERROR',
|
||||
message: 'Task ID (id) is required'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (!args.dependsOn) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'INPUT_VALIDATION_ERROR',
|
||||
message: 'Dependency ID (dependsOn) is required'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Find the tasks.json path
|
||||
const tasksPath = findTasksJsonPath(args, log);
|
||||
|
||||
// Format IDs for the core function
|
||||
const taskId = args.id.includes && args.id.includes('.') ? args.id : parseInt(args.id, 10);
|
||||
const dependencyId = args.dependsOn.includes && args.dependsOn.includes('.') ? args.dependsOn : parseInt(args.dependsOn, 10);
|
||||
|
||||
log.info(`Removing dependency: task ${taskId} no longer depends on ${dependencyId}`);
|
||||
|
||||
// Enable silent mode to prevent console logs from interfering with JSON response
|
||||
enableSilentMode();
|
||||
|
||||
// Call the core function
|
||||
await removeDependency(tasksPath, taskId, dependencyId);
|
||||
|
||||
// Restore normal logging
|
||||
disableSilentMode();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
message: `Successfully removed dependency: Task ${taskId} no longer depends on ${dependencyId}`,
|
||||
taskId: taskId,
|
||||
dependencyId: dependencyId
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
// Make sure to restore normal logging even if there's an error
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Error in removeDependencyDirect: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'CORE_FUNCTION_ERROR',
|
||||
message: error.message
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
try {
|
||||
log.info(`Removing dependency with args: ${JSON.stringify(args)}`);
|
||||
|
||||
// Validate required parameters
|
||||
if (!args.id) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'INPUT_VALIDATION_ERROR',
|
||||
message: 'Task ID (id) is required'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (!args.dependsOn) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'INPUT_VALIDATION_ERROR',
|
||||
message: 'Dependency ID (dependsOn) is required'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Find the tasks.json path
|
||||
const tasksPath = findTasksJsonPath(args, log);
|
||||
|
||||
// Format IDs for the core function
|
||||
const taskId =
|
||||
args.id.includes && args.id.includes('.')
|
||||
? args.id
|
||||
: parseInt(args.id, 10);
|
||||
const dependencyId =
|
||||
args.dependsOn.includes && args.dependsOn.includes('.')
|
||||
? args.dependsOn
|
||||
: parseInt(args.dependsOn, 10);
|
||||
|
||||
log.info(
|
||||
`Removing dependency: task ${taskId} no longer depends on ${dependencyId}`
|
||||
);
|
||||
|
||||
// Enable silent mode to prevent console logs from interfering with JSON response
|
||||
enableSilentMode();
|
||||
|
||||
// Call the core function
|
||||
await removeDependency(tasksPath, taskId, dependencyId);
|
||||
|
||||
// Restore normal logging
|
||||
disableSilentMode();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
message: `Successfully removed dependency: Task ${taskId} no longer depends on ${dependencyId}`,
|
||||
taskId: taskId,
|
||||
dependencyId: dependencyId
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
// Make sure to restore normal logging even if there's an error
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Error in removeDependencyDirect: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'CORE_FUNCTION_ERROR',
|
||||
message: error.message
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
|
||||
import { removeSubtask } from '../../../../scripts/modules/task-manager.js';
|
||||
import { findTasksJsonPath } from '../utils/path-utils.js';
|
||||
import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js';
|
||||
import {
|
||||
enableSilentMode,
|
||||
disableSilentMode
|
||||
} from '../../../../scripts/modules/utils.js';
|
||||
|
||||
/**
|
||||
* Remove a subtask from its parent task
|
||||
@@ -18,78 +21,86 @@ import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules
|
||||
* @returns {Promise<{success: boolean, data?: Object, error?: {code: string, message: string}}>}
|
||||
*/
|
||||
export async function removeSubtaskDirect(args, log) {
|
||||
try {
|
||||
// Enable silent mode to prevent console logs from interfering with JSON response
|
||||
enableSilentMode();
|
||||
|
||||
log.info(`Removing subtask with args: ${JSON.stringify(args)}`);
|
||||
|
||||
if (!args.id) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'INPUT_VALIDATION_ERROR',
|
||||
message: 'Subtask ID is required and must be in format "parentId.subtaskId"'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Validate subtask ID format
|
||||
if (!args.id.includes('.')) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'INPUT_VALIDATION_ERROR',
|
||||
message: `Invalid subtask ID format: ${args.id}. Expected format: "parentId.subtaskId"`
|
||||
}
|
||||
};
|
||||
}
|
||||
try {
|
||||
// Enable silent mode to prevent console logs from interfering with JSON response
|
||||
enableSilentMode();
|
||||
|
||||
// Find the tasks.json path
|
||||
const tasksPath = findTasksJsonPath(args, log);
|
||||
|
||||
// Convert convertToTask to a boolean
|
||||
const convertToTask = args.convert === true;
|
||||
|
||||
// Determine if we should generate files
|
||||
const generateFiles = !args.skipGenerate;
|
||||
|
||||
log.info(`Removing subtask ${args.id} (convertToTask: ${convertToTask}, generateFiles: ${generateFiles})`);
|
||||
|
||||
const result = await removeSubtask(tasksPath, args.id, convertToTask, generateFiles);
|
||||
|
||||
// Restore normal logging
|
||||
disableSilentMode();
|
||||
|
||||
if (convertToTask && result) {
|
||||
// Return info about the converted task
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
message: `Subtask ${args.id} successfully converted to task #${result.id}`,
|
||||
task: result
|
||||
}
|
||||
};
|
||||
} else {
|
||||
// Return simple success message for deletion
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
message: `Subtask ${args.id} successfully removed`
|
||||
}
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
// Ensure silent mode is disabled even if an outer error occurs
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Error in removeSubtaskDirect: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'CORE_FUNCTION_ERROR',
|
||||
message: error.message
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
log.info(`Removing subtask with args: ${JSON.stringify(args)}`);
|
||||
|
||||
if (!args.id) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'INPUT_VALIDATION_ERROR',
|
||||
message:
|
||||
'Subtask ID is required and must be in format "parentId.subtaskId"'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Validate subtask ID format
|
||||
if (!args.id.includes('.')) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'INPUT_VALIDATION_ERROR',
|
||||
message: `Invalid subtask ID format: ${args.id}. Expected format: "parentId.subtaskId"`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Find the tasks.json path
|
||||
const tasksPath = findTasksJsonPath(args, log);
|
||||
|
||||
// Convert convertToTask to a boolean
|
||||
const convertToTask = args.convert === true;
|
||||
|
||||
// Determine if we should generate files
|
||||
const generateFiles = !args.skipGenerate;
|
||||
|
||||
log.info(
|
||||
`Removing subtask ${args.id} (convertToTask: ${convertToTask}, generateFiles: ${generateFiles})`
|
||||
);
|
||||
|
||||
const result = await removeSubtask(
|
||||
tasksPath,
|
||||
args.id,
|
||||
convertToTask,
|
||||
generateFiles
|
||||
);
|
||||
|
||||
// Restore normal logging
|
||||
disableSilentMode();
|
||||
|
||||
if (convertToTask && result) {
|
||||
// Return info about the converted task
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
message: `Subtask ${args.id} successfully converted to task #${result.id}`,
|
||||
task: result
|
||||
}
|
||||
};
|
||||
} else {
|
||||
// Return simple success message for deletion
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
message: `Subtask ${args.id} successfully removed`
|
||||
}
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
// Ensure silent mode is disabled even if an outer error occurs
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Error in removeSubtaskDirect: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'CORE_FUNCTION_ERROR',
|
||||
message: error.message
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
*/
|
||||
|
||||
import { removeTask } from '../../../../scripts/modules/task-manager.js';
|
||||
import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js';
|
||||
import {
|
||||
enableSilentMode,
|
||||
disableSilentMode
|
||||
} from '../../../../scripts/modules/utils.js';
|
||||
import { findTasksJsonPath } from '../utils/path-utils.js';
|
||||
|
||||
/**
|
||||
@@ -15,90 +18,90 @@ import { findTasksJsonPath } from '../utils/path-utils.js';
|
||||
* @returns {Promise<Object>} - Remove task result { success: boolean, data?: any, error?: { code: string, message: string }, fromCache: false }
|
||||
*/
|
||||
export async function removeTaskDirect(args, log) {
|
||||
try {
|
||||
// Find the tasks path first
|
||||
let tasksPath;
|
||||
try {
|
||||
tasksPath = findTasksJsonPath(args, log);
|
||||
} catch (error) {
|
||||
log.error(`Tasks file not found: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'FILE_NOT_FOUND_ERROR',
|
||||
message: error.message
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Validate task ID parameter
|
||||
const taskId = args.id;
|
||||
if (!taskId) {
|
||||
log.error('Task ID is required');
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'INPUT_VALIDATION_ERROR',
|
||||
message: 'Task ID is required'
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Skip confirmation in the direct function since it's handled by the client
|
||||
log.info(`Removing task with ID: ${taskId} from ${tasksPath}`);
|
||||
|
||||
try {
|
||||
// Enable silent mode to prevent console logs from interfering with JSON response
|
||||
enableSilentMode();
|
||||
|
||||
// Call the core removeTask function
|
||||
const result = await removeTask(tasksPath, taskId);
|
||||
|
||||
// Restore normal logging
|
||||
disableSilentMode();
|
||||
|
||||
log.info(`Successfully removed task: ${taskId}`);
|
||||
|
||||
// Return the result
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
message: result.message,
|
||||
taskId: taskId,
|
||||
tasksPath: tasksPath,
|
||||
removedTask: result.removedTask
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
} catch (error) {
|
||||
// Make sure to restore normal logging even if there's an error
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Error removing task: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: error.code || 'REMOVE_TASK_ERROR',
|
||||
message: error.message || 'Failed to remove task'
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
// Ensure silent mode is disabled even if an outer error occurs
|
||||
disableSilentMode();
|
||||
|
||||
// Catch any unexpected errors
|
||||
log.error(`Unexpected error in removeTaskDirect: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'UNEXPECTED_ERROR',
|
||||
message: error.message
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
}
|
||||
try {
|
||||
// Find the tasks path first
|
||||
let tasksPath;
|
||||
try {
|
||||
tasksPath = findTasksJsonPath(args, log);
|
||||
} catch (error) {
|
||||
log.error(`Tasks file not found: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'FILE_NOT_FOUND_ERROR',
|
||||
message: error.message
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Validate task ID parameter
|
||||
const taskId = args.id;
|
||||
if (!taskId) {
|
||||
log.error('Task ID is required');
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'INPUT_VALIDATION_ERROR',
|
||||
message: 'Task ID is required'
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Skip confirmation in the direct function since it's handled by the client
|
||||
log.info(`Removing task with ID: ${taskId} from ${tasksPath}`);
|
||||
|
||||
try {
|
||||
// Enable silent mode to prevent console logs from interfering with JSON response
|
||||
enableSilentMode();
|
||||
|
||||
// Call the core removeTask function
|
||||
const result = await removeTask(tasksPath, taskId);
|
||||
|
||||
// Restore normal logging
|
||||
disableSilentMode();
|
||||
|
||||
log.info(`Successfully removed task: ${taskId}`);
|
||||
|
||||
// Return the result
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
message: result.message,
|
||||
taskId: taskId,
|
||||
tasksPath: tasksPath,
|
||||
removedTask: result.removedTask
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
} catch (error) {
|
||||
// Make sure to restore normal logging even if there's an error
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Error removing task: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: error.code || 'REMOVE_TASK_ERROR',
|
||||
message: error.message || 'Failed to remove task'
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
// Ensure silent mode is disabled even if an outer error occurs
|
||||
disableSilentMode();
|
||||
|
||||
// Catch any unexpected errors
|
||||
log.error(`Unexpected error in removeTaskDirect: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'UNEXPECTED_ERROR',
|
||||
message: error.message
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,108 +5,120 @@
|
||||
|
||||
import { setTaskStatus } from '../../../../scripts/modules/task-manager.js';
|
||||
import { findTasksJsonPath } from '../utils/path-utils.js';
|
||||
import { enableSilentMode, disableSilentMode, isSilentMode } from '../../../../scripts/modules/utils.js';
|
||||
import {
|
||||
enableSilentMode,
|
||||
disableSilentMode,
|
||||
isSilentMode
|
||||
} from '../../../../scripts/modules/utils.js';
|
||||
|
||||
/**
|
||||
* Direct function wrapper for setTaskStatus with error handling.
|
||||
*
|
||||
*
|
||||
* @param {Object} args - Command arguments containing id, status and file path options.
|
||||
* @param {Object} log - Logger object.
|
||||
* @returns {Promise<Object>} - Result object with success status and data/error information.
|
||||
*/
|
||||
export async function setTaskStatusDirect(args, log) {
|
||||
try {
|
||||
log.info(`Setting task status with args: ${JSON.stringify(args)}`);
|
||||
|
||||
// Check required parameters
|
||||
if (!args.id) {
|
||||
const errorMessage = 'No task ID specified. Please provide a task ID to update.';
|
||||
log.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'MISSING_TASK_ID', message: errorMessage },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
if (!args.status) {
|
||||
const errorMessage = 'No status specified. Please provide a new status value.';
|
||||
log.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'MISSING_STATUS', message: errorMessage },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Get tasks file path
|
||||
let tasksPath;
|
||||
try {
|
||||
// The enhanced findTasksJsonPath will now search in parent directories if needed
|
||||
tasksPath = findTasksJsonPath(args, log);
|
||||
log.info(`Found tasks file at: ${tasksPath}`);
|
||||
} catch (error) {
|
||||
log.error(`Error finding tasks file: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'TASKS_FILE_ERROR',
|
||||
message: `${error.message}\n\nPlease ensure you are in a Task Master project directory or use the --project-root parameter to specify the path to your project.`
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Execute core setTaskStatus function
|
||||
const taskId = args.id;
|
||||
const newStatus = args.status;
|
||||
|
||||
log.info(`Setting task ${taskId} status to "${newStatus}"`);
|
||||
|
||||
// Call the core function with proper silent mode handling
|
||||
let result;
|
||||
enableSilentMode(); // Enable silent mode before calling core function
|
||||
try {
|
||||
// Call the core function
|
||||
await setTaskStatus(tasksPath, taskId, newStatus, { mcpLog: log });
|
||||
|
||||
log.info(`Successfully set task ${taskId} status to ${newStatus}`);
|
||||
|
||||
// Return success data
|
||||
result = {
|
||||
success: true,
|
||||
data: {
|
||||
message: `Successfully updated task ${taskId} status to "${newStatus}"`,
|
||||
taskId,
|
||||
status: newStatus,
|
||||
tasksPath
|
||||
},
|
||||
fromCache: false // This operation always modifies state and should never be cached
|
||||
};
|
||||
} catch (error) {
|
||||
log.error(`Error setting task status: ${error.message}`);
|
||||
result = {
|
||||
success: false,
|
||||
error: { code: 'SET_STATUS_ERROR', message: error.message || 'Unknown error setting task status' },
|
||||
fromCache: false
|
||||
};
|
||||
} finally {
|
||||
// ALWAYS restore normal logging in finally block
|
||||
disableSilentMode();
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
// Ensure silent mode is disabled if there was an uncaught error in the outer try block
|
||||
if (isSilentMode()) {
|
||||
disableSilentMode();
|
||||
}
|
||||
|
||||
log.error(`Error setting task status: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'SET_STATUS_ERROR', message: error.message || 'Unknown error setting task status' },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
}
|
||||
try {
|
||||
log.info(`Setting task status with args: ${JSON.stringify(args)}`);
|
||||
|
||||
// Check required parameters
|
||||
if (!args.id) {
|
||||
const errorMessage =
|
||||
'No task ID specified. Please provide a task ID to update.';
|
||||
log.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'MISSING_TASK_ID', message: errorMessage },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
if (!args.status) {
|
||||
const errorMessage =
|
||||
'No status specified. Please provide a new status value.';
|
||||
log.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'MISSING_STATUS', message: errorMessage },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Get tasks file path
|
||||
let tasksPath;
|
||||
try {
|
||||
// The enhanced findTasksJsonPath will now search in parent directories if needed
|
||||
tasksPath = findTasksJsonPath(args, log);
|
||||
log.info(`Found tasks file at: ${tasksPath}`);
|
||||
} catch (error) {
|
||||
log.error(`Error finding tasks file: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'TASKS_FILE_ERROR',
|
||||
message: `${error.message}\n\nPlease ensure you are in a Task Master project directory or use the --project-root parameter to specify the path to your project.`
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Execute core setTaskStatus function
|
||||
const taskId = args.id;
|
||||
const newStatus = args.status;
|
||||
|
||||
log.info(`Setting task ${taskId} status to "${newStatus}"`);
|
||||
|
||||
// Call the core function with proper silent mode handling
|
||||
let result;
|
||||
enableSilentMode(); // Enable silent mode before calling core function
|
||||
try {
|
||||
// Call the core function
|
||||
await setTaskStatus(tasksPath, taskId, newStatus, { mcpLog: log });
|
||||
|
||||
log.info(`Successfully set task ${taskId} status to ${newStatus}`);
|
||||
|
||||
// Return success data
|
||||
result = {
|
||||
success: true,
|
||||
data: {
|
||||
message: `Successfully updated task ${taskId} status to "${newStatus}"`,
|
||||
taskId,
|
||||
status: newStatus,
|
||||
tasksPath
|
||||
},
|
||||
fromCache: false // This operation always modifies state and should never be cached
|
||||
};
|
||||
} catch (error) {
|
||||
log.error(`Error setting task status: ${error.message}`);
|
||||
result = {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'SET_STATUS_ERROR',
|
||||
message: error.message || 'Unknown error setting task status'
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
} finally {
|
||||
// ALWAYS restore normal logging in finally block
|
||||
disableSilentMode();
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
// Ensure silent mode is disabled if there was an uncaught error in the outer try block
|
||||
if (isSilentMode()) {
|
||||
disableSilentMode();
|
||||
}
|
||||
|
||||
log.error(`Error setting task status: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'SET_STATUS_ERROR',
|
||||
message: error.message || 'Unknown error setting task status'
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,10 @@ import { findTaskById } from '../../../../scripts/modules/utils.js';
|
||||
import { readJSON } from '../../../../scripts/modules/utils.js';
|
||||
import { getCachedOrExecute } from '../../tools/utils.js';
|
||||
import { findTasksJsonPath } from '../utils/path-utils.js';
|
||||
import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js';
|
||||
import {
|
||||
enableSilentMode,
|
||||
disableSilentMode
|
||||
} from '../../../../scripts/modules/utils.js';
|
||||
|
||||
/**
|
||||
* Direct function wrapper for showing task details with error handling and caching.
|
||||
@@ -17,120 +20,122 @@ import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules
|
||||
* @returns {Promise<Object>} - Task details result { success: boolean, data?: any, error?: { code: string, message: string }, fromCache: boolean }
|
||||
*/
|
||||
export async function showTaskDirect(args, log) {
|
||||
let tasksPath;
|
||||
try {
|
||||
// Find the tasks path first - needed for cache key and execution
|
||||
tasksPath = findTasksJsonPath(args, log);
|
||||
} catch (error) {
|
||||
log.error(`Tasks file not found: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'FILE_NOT_FOUND_ERROR',
|
||||
message: error.message
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
let tasksPath;
|
||||
try {
|
||||
// Find the tasks path first - needed for cache key and execution
|
||||
tasksPath = findTasksJsonPath(args, log);
|
||||
} catch (error) {
|
||||
log.error(`Tasks file not found: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'FILE_NOT_FOUND_ERROR',
|
||||
message: error.message
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Validate task ID
|
||||
const taskId = args.id;
|
||||
if (!taskId) {
|
||||
log.error('Task ID is required');
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'INPUT_VALIDATION_ERROR',
|
||||
message: 'Task ID is required'
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
// Validate task ID
|
||||
const taskId = args.id;
|
||||
if (!taskId) {
|
||||
log.error('Task ID is required');
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'INPUT_VALIDATION_ERROR',
|
||||
message: 'Task ID is required'
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Generate cache key using task path and ID
|
||||
const cacheKey = `showTask:${tasksPath}:${taskId}`;
|
||||
|
||||
// Define the action function to be executed on cache miss
|
||||
const coreShowTaskAction = async () => {
|
||||
try {
|
||||
// Enable silent mode to prevent console logs from interfering with JSON response
|
||||
enableSilentMode();
|
||||
|
||||
log.info(`Retrieving task details for ID: ${taskId} from ${tasksPath}`);
|
||||
|
||||
// Read tasks data
|
||||
const data = readJSON(tasksPath);
|
||||
if (!data || !data.tasks) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'INVALID_TASKS_FILE',
|
||||
message: `No valid tasks found in ${tasksPath}`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Find the specific task
|
||||
const task = findTaskById(data.tasks, taskId);
|
||||
|
||||
if (!task) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'TASK_NOT_FOUND',
|
||||
message: `Task with ID ${taskId} not found`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Restore normal logging
|
||||
disableSilentMode();
|
||||
|
||||
// Return the task data with the full tasks array for reference
|
||||
// (needed for formatDependenciesWithStatus function in UI)
|
||||
log.info(`Successfully found task ${taskId}`);
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
task,
|
||||
allTasks: data.tasks
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
// Make sure to restore normal logging even if there's an error
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Error showing task: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'CORE_FUNCTION_ERROR',
|
||||
message: error.message || 'Failed to show task details'
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
// Generate cache key using task path and ID
|
||||
const cacheKey = `showTask:${tasksPath}:${taskId}`;
|
||||
|
||||
// Use the caching utility
|
||||
try {
|
||||
const result = await getCachedOrExecute({
|
||||
cacheKey,
|
||||
actionFn: coreShowTaskAction,
|
||||
log
|
||||
});
|
||||
log.info(`showTaskDirect completed. From cache: ${result.fromCache}`);
|
||||
return result; // Returns { success, data/error, fromCache }
|
||||
} catch (error) {
|
||||
// Catch unexpected errors from getCachedOrExecute itself
|
||||
disableSilentMode();
|
||||
log.error(`Unexpected error during getCachedOrExecute for showTask: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'UNEXPECTED_ERROR',
|
||||
message: error.message
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
}
|
||||
// Define the action function to be executed on cache miss
|
||||
const coreShowTaskAction = async () => {
|
||||
try {
|
||||
// Enable silent mode to prevent console logs from interfering with JSON response
|
||||
enableSilentMode();
|
||||
|
||||
log.info(`Retrieving task details for ID: ${taskId} from ${tasksPath}`);
|
||||
|
||||
// Read tasks data
|
||||
const data = readJSON(tasksPath);
|
||||
if (!data || !data.tasks) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'INVALID_TASKS_FILE',
|
||||
message: `No valid tasks found in ${tasksPath}`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Find the specific task
|
||||
const task = findTaskById(data.tasks, taskId);
|
||||
|
||||
if (!task) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'TASK_NOT_FOUND',
|
||||
message: `Task with ID ${taskId} not found`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Restore normal logging
|
||||
disableSilentMode();
|
||||
|
||||
// Return the task data with the full tasks array for reference
|
||||
// (needed for formatDependenciesWithStatus function in UI)
|
||||
log.info(`Successfully found task ${taskId}`);
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
task,
|
||||
allTasks: data.tasks
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
// Make sure to restore normal logging even if there's an error
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Error showing task: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'CORE_FUNCTION_ERROR',
|
||||
message: error.message || 'Failed to show task details'
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Use the caching utility
|
||||
try {
|
||||
const result = await getCachedOrExecute({
|
||||
cacheKey,
|
||||
actionFn: coreShowTaskAction,
|
||||
log
|
||||
});
|
||||
log.info(`showTaskDirect completed. From cache: ${result.fromCache}`);
|
||||
return result; // Returns { success, data/error, fromCache }
|
||||
} catch (error) {
|
||||
// Catch unexpected errors from getCachedOrExecute itself
|
||||
disableSilentMode();
|
||||
log.error(
|
||||
`Unexpected error during getCachedOrExecute for showTask: ${error.message}`
|
||||
);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'UNEXPECTED_ERROR',
|
||||
message: error.message
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,167 +4,190 @@
|
||||
*/
|
||||
|
||||
import { updateSubtaskById } from '../../../../scripts/modules/task-manager.js';
|
||||
import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js';
|
||||
import {
|
||||
enableSilentMode,
|
||||
disableSilentMode
|
||||
} from '../../../../scripts/modules/utils.js';
|
||||
import { findTasksJsonPath } from '../utils/path-utils.js';
|
||||
import { getAnthropicClientForMCP, getPerplexityClientForMCP } from '../utils/ai-client-utils.js';
|
||||
import {
|
||||
getAnthropicClientForMCP,
|
||||
getPerplexityClientForMCP
|
||||
} from '../utils/ai-client-utils.js';
|
||||
|
||||
/**
|
||||
* Direct function wrapper for updateSubtaskById with error handling.
|
||||
*
|
||||
*
|
||||
* @param {Object} args - Command arguments containing id, prompt, useResearch and file path options.
|
||||
* @param {Object} log - Logger object.
|
||||
* @param {Object} context - Context object containing session data.
|
||||
* @returns {Promise<Object>} - Result object with success status and data/error information.
|
||||
*/
|
||||
export async function updateSubtaskByIdDirect(args, log, context = {}) {
|
||||
const { session } = context; // Only extract session, not reportProgress
|
||||
|
||||
try {
|
||||
log.info(`Updating subtask with args: ${JSON.stringify(args)}`);
|
||||
|
||||
// Check required parameters
|
||||
if (!args.id) {
|
||||
const errorMessage = 'No subtask ID specified. Please provide a subtask ID to update.';
|
||||
log.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'MISSING_SUBTASK_ID', message: errorMessage },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
if (!args.prompt) {
|
||||
const errorMessage = 'No prompt specified. Please provide a prompt with information to add to the subtask.';
|
||||
log.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'MISSING_PROMPT', message: errorMessage },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Validate subtask ID format
|
||||
const subtaskId = args.id;
|
||||
if (typeof subtaskId !== 'string' && typeof subtaskId !== 'number') {
|
||||
const errorMessage = `Invalid subtask ID type: ${typeof subtaskId}. Subtask ID must be a string or number.`;
|
||||
log.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'INVALID_SUBTASK_ID_TYPE', message: errorMessage },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
const subtaskIdStr = String(subtaskId);
|
||||
if (!subtaskIdStr.includes('.')) {
|
||||
const errorMessage = `Invalid subtask ID format: ${subtaskIdStr}. Subtask ID must be in format "parentId.subtaskId" (e.g., "5.2").`;
|
||||
log.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'INVALID_SUBTASK_ID_FORMAT', message: errorMessage },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Get tasks file path
|
||||
let tasksPath;
|
||||
try {
|
||||
tasksPath = findTasksJsonPath(args, log);
|
||||
} catch (error) {
|
||||
log.error(`Error finding tasks file: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'TASKS_FILE_ERROR', message: error.message },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Get research flag
|
||||
const useResearch = args.research === true;
|
||||
|
||||
log.info(`Updating subtask with ID ${subtaskIdStr} with prompt "${args.prompt}" and research: ${useResearch}`);
|
||||
|
||||
// Initialize the appropriate AI client based on research flag
|
||||
try {
|
||||
if (useResearch) {
|
||||
// Initialize Perplexity client
|
||||
await getPerplexityClientForMCP(session);
|
||||
} else {
|
||||
// Initialize Anthropic client
|
||||
await getAnthropicClientForMCP(session);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`AI client initialization error: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'AI_CLIENT_ERROR', message: error.message || 'Failed to initialize AI client' },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// Enable silent mode to prevent console logs from interfering with JSON response
|
||||
enableSilentMode();
|
||||
|
||||
// Create a logger wrapper object to handle logging without breaking the mcpLog[level] calls
|
||||
// This ensures outputFormat is set to 'json' while still supporting proper logging
|
||||
const logWrapper = {
|
||||
info: (message) => log.info(message),
|
||||
warn: (message) => log.warn(message),
|
||||
error: (message) => log.error(message),
|
||||
debug: (message) => log.debug && log.debug(message),
|
||||
success: (message) => log.info(message) // Map success to info if needed
|
||||
};
|
||||
|
||||
// Execute core updateSubtaskById function
|
||||
// Pass both session and logWrapper as mcpLog to ensure outputFormat is 'json'
|
||||
const updatedSubtask = await updateSubtaskById(tasksPath, subtaskIdStr, args.prompt, useResearch, {
|
||||
session,
|
||||
mcpLog: logWrapper
|
||||
});
|
||||
|
||||
// Restore normal logging
|
||||
disableSilentMode();
|
||||
|
||||
// Handle the case where the subtask couldn't be updated (e.g., already marked as done)
|
||||
if (!updatedSubtask) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'SUBTASK_UPDATE_FAILED',
|
||||
message: 'Failed to update subtask. It may be marked as completed, or another error occurred.'
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Return the updated subtask information
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
message: `Successfully updated subtask with ID ${subtaskIdStr}`,
|
||||
subtaskId: subtaskIdStr,
|
||||
parentId: subtaskIdStr.split('.')[0],
|
||||
subtask: updatedSubtask,
|
||||
tasksPath,
|
||||
useResearch
|
||||
},
|
||||
fromCache: false // This operation always modifies state and should never be cached
|
||||
};
|
||||
} catch (error) {
|
||||
// Make sure to restore normal logging even if there's an error
|
||||
disableSilentMode();
|
||||
throw error; // Rethrow to be caught by outer catch block
|
||||
}
|
||||
} catch (error) {
|
||||
// Ensure silent mode is disabled
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Error updating subtask by ID: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'UPDATE_SUBTASK_ERROR', message: error.message || 'Unknown error updating subtask' },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
}
|
||||
const { session } = context; // Only extract session, not reportProgress
|
||||
|
||||
try {
|
||||
log.info(`Updating subtask with args: ${JSON.stringify(args)}`);
|
||||
|
||||
// Check required parameters
|
||||
if (!args.id) {
|
||||
const errorMessage =
|
||||
'No subtask ID specified. Please provide a subtask ID to update.';
|
||||
log.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'MISSING_SUBTASK_ID', message: errorMessage },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
if (!args.prompt) {
|
||||
const errorMessage =
|
||||
'No prompt specified. Please provide a prompt with information to add to the subtask.';
|
||||
log.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'MISSING_PROMPT', message: errorMessage },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Validate subtask ID format
|
||||
const subtaskId = args.id;
|
||||
if (typeof subtaskId !== 'string' && typeof subtaskId !== 'number') {
|
||||
const errorMessage = `Invalid subtask ID type: ${typeof subtaskId}. Subtask ID must be a string or number.`;
|
||||
log.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'INVALID_SUBTASK_ID_TYPE', message: errorMessage },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
const subtaskIdStr = String(subtaskId);
|
||||
if (!subtaskIdStr.includes('.')) {
|
||||
const errorMessage = `Invalid subtask ID format: ${subtaskIdStr}. Subtask ID must be in format "parentId.subtaskId" (e.g., "5.2").`;
|
||||
log.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'INVALID_SUBTASK_ID_FORMAT', message: errorMessage },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Get tasks file path
|
||||
let tasksPath;
|
||||
try {
|
||||
tasksPath = findTasksJsonPath(args, log);
|
||||
} catch (error) {
|
||||
log.error(`Error finding tasks file: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'TASKS_FILE_ERROR', message: error.message },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Get research flag
|
||||
const useResearch = args.research === true;
|
||||
|
||||
log.info(
|
||||
`Updating subtask with ID ${subtaskIdStr} with prompt "${args.prompt}" and research: ${useResearch}`
|
||||
);
|
||||
|
||||
// Initialize the appropriate AI client based on research flag
|
||||
try {
|
||||
if (useResearch) {
|
||||
// Initialize Perplexity client
|
||||
await getPerplexityClientForMCP(session);
|
||||
} else {
|
||||
// Initialize Anthropic client
|
||||
await getAnthropicClientForMCP(session);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`AI client initialization error: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'AI_CLIENT_ERROR',
|
||||
message: error.message || 'Failed to initialize AI client'
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// Enable silent mode to prevent console logs from interfering with JSON response
|
||||
enableSilentMode();
|
||||
|
||||
// Create a logger wrapper object to handle logging without breaking the mcpLog[level] calls
|
||||
// This ensures outputFormat is set to 'json' while still supporting proper logging
|
||||
const logWrapper = {
|
||||
info: (message) => log.info(message),
|
||||
warn: (message) => log.warn(message),
|
||||
error: (message) => log.error(message),
|
||||
debug: (message) => log.debug && log.debug(message),
|
||||
success: (message) => log.info(message) // Map success to info if needed
|
||||
};
|
||||
|
||||
// Execute core updateSubtaskById function
|
||||
// Pass both session and logWrapper as mcpLog to ensure outputFormat is 'json'
|
||||
const updatedSubtask = await updateSubtaskById(
|
||||
tasksPath,
|
||||
subtaskIdStr,
|
||||
args.prompt,
|
||||
useResearch,
|
||||
{
|
||||
session,
|
||||
mcpLog: logWrapper
|
||||
}
|
||||
);
|
||||
|
||||
// Restore normal logging
|
||||
disableSilentMode();
|
||||
|
||||
// Handle the case where the subtask couldn't be updated (e.g., already marked as done)
|
||||
if (!updatedSubtask) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'SUBTASK_UPDATE_FAILED',
|
||||
message:
|
||||
'Failed to update subtask. It may be marked as completed, or another error occurred.'
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Return the updated subtask information
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
message: `Successfully updated subtask with ID ${subtaskIdStr}`,
|
||||
subtaskId: subtaskIdStr,
|
||||
parentId: subtaskIdStr.split('.')[0],
|
||||
subtask: updatedSubtask,
|
||||
tasksPath,
|
||||
useResearch
|
||||
},
|
||||
fromCache: false // This operation always modifies state and should never be cached
|
||||
};
|
||||
} catch (error) {
|
||||
// Make sure to restore normal logging even if there's an error
|
||||
disableSilentMode();
|
||||
throw error; // Rethrow to be caught by outer catch block
|
||||
}
|
||||
} catch (error) {
|
||||
// Ensure silent mode is disabled
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Error updating subtask by ID: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'UPDATE_SUBTASK_ERROR',
|
||||
message: error.message || 'Unknown error updating subtask'
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,168 +5,181 @@
|
||||
|
||||
import { updateTaskById } from '../../../../scripts/modules/task-manager.js';
|
||||
import { findTasksJsonPath } from '../utils/path-utils.js';
|
||||
import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js';
|
||||
import {
|
||||
getAnthropicClientForMCP,
|
||||
getPerplexityClientForMCP
|
||||
import {
|
||||
enableSilentMode,
|
||||
disableSilentMode
|
||||
} from '../../../../scripts/modules/utils.js';
|
||||
import {
|
||||
getAnthropicClientForMCP,
|
||||
getPerplexityClientForMCP
|
||||
} from '../utils/ai-client-utils.js';
|
||||
|
||||
/**
|
||||
* Direct function wrapper for updateTaskById with error handling.
|
||||
*
|
||||
*
|
||||
* @param {Object} args - Command arguments containing id, prompt, useResearch and file path options.
|
||||
* @param {Object} log - Logger object.
|
||||
* @param {Object} context - Context object containing session data.
|
||||
* @returns {Promise<Object>} - Result object with success status and data/error information.
|
||||
*/
|
||||
export async function updateTaskByIdDirect(args, log, context = {}) {
|
||||
const { session } = context; // Only extract session, not reportProgress
|
||||
|
||||
try {
|
||||
log.info(`Updating task with args: ${JSON.stringify(args)}`);
|
||||
|
||||
// Check required parameters
|
||||
if (!args.id) {
|
||||
const errorMessage = 'No task ID specified. Please provide a task ID to update.';
|
||||
log.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'MISSING_TASK_ID', message: errorMessage },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
if (!args.prompt) {
|
||||
const errorMessage = 'No prompt specified. Please provide a prompt with new information for the task update.';
|
||||
log.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'MISSING_PROMPT', message: errorMessage },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Parse taskId - handle both string and number values
|
||||
let taskId;
|
||||
if (typeof args.id === 'string') {
|
||||
// Handle subtask IDs (e.g., "5.2")
|
||||
if (args.id.includes('.')) {
|
||||
taskId = args.id; // Keep as string for subtask IDs
|
||||
} else {
|
||||
// Parse as integer for main task IDs
|
||||
taskId = parseInt(args.id, 10);
|
||||
if (isNaN(taskId)) {
|
||||
const errorMessage = `Invalid task ID: ${args.id}. Task ID must be a positive integer or subtask ID (e.g., "5.2").`;
|
||||
log.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'INVALID_TASK_ID', message: errorMessage },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
}
|
||||
} else {
|
||||
taskId = args.id;
|
||||
}
|
||||
|
||||
// Get tasks file path
|
||||
let tasksPath;
|
||||
try {
|
||||
tasksPath = findTasksJsonPath(args, log);
|
||||
} catch (error) {
|
||||
log.error(`Error finding tasks file: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'TASKS_FILE_ERROR', message: error.message },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Get research flag
|
||||
const useResearch = args.research === true;
|
||||
|
||||
// Initialize appropriate AI client based on research flag
|
||||
let aiClient;
|
||||
try {
|
||||
if (useResearch) {
|
||||
log.info('Using Perplexity AI for research-backed task update');
|
||||
aiClient = await getPerplexityClientForMCP(session, log);
|
||||
} else {
|
||||
log.info('Using Claude AI for task update');
|
||||
aiClient = getAnthropicClientForMCP(session, log);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`Failed to initialize AI client: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'AI_CLIENT_ERROR',
|
||||
message: `Cannot initialize AI client: ${error.message}`
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
log.info(`Updating task with ID ${taskId} with prompt "${args.prompt}" and research: ${useResearch}`);
|
||||
|
||||
try {
|
||||
// Enable silent mode to prevent console logs from interfering with JSON response
|
||||
enableSilentMode();
|
||||
|
||||
// Create a logger wrapper that matches what updateTaskById expects
|
||||
const logWrapper = {
|
||||
info: (message) => log.info(message),
|
||||
warn: (message) => log.warn(message),
|
||||
error: (message) => log.error(message),
|
||||
debug: (message) => log.debug && log.debug(message),
|
||||
success: (message) => log.info(message) // Map success to info since many loggers don't have success
|
||||
};
|
||||
|
||||
// Execute core updateTaskById function with proper parameters
|
||||
await updateTaskById(
|
||||
tasksPath,
|
||||
taskId,
|
||||
args.prompt,
|
||||
useResearch,
|
||||
{
|
||||
mcpLog: logWrapper, // Use our wrapper object that has the expected method structure
|
||||
session
|
||||
},
|
||||
'json'
|
||||
);
|
||||
|
||||
// Since updateTaskById doesn't return a value but modifies the tasks file,
|
||||
// we'll return a success message
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
message: `Successfully updated task with ID ${taskId} based on the prompt`,
|
||||
taskId,
|
||||
tasksPath,
|
||||
useResearch
|
||||
},
|
||||
fromCache: false // This operation always modifies state and should never be cached
|
||||
};
|
||||
} catch (error) {
|
||||
log.error(`Error updating task by ID: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'UPDATE_TASK_ERROR', message: error.message || 'Unknown error updating task' },
|
||||
fromCache: false
|
||||
};
|
||||
} finally {
|
||||
// Make sure to restore normal logging even if there's an error
|
||||
disableSilentMode();
|
||||
}
|
||||
} catch (error) {
|
||||
// Ensure silent mode is disabled
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Error updating task by ID: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'UPDATE_TASK_ERROR', message: error.message || 'Unknown error updating task' },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
}
|
||||
const { session } = context; // Only extract session, not reportProgress
|
||||
|
||||
try {
|
||||
log.info(`Updating task with args: ${JSON.stringify(args)}`);
|
||||
|
||||
// Check required parameters
|
||||
if (!args.id) {
|
||||
const errorMessage =
|
||||
'No task ID specified. Please provide a task ID to update.';
|
||||
log.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'MISSING_TASK_ID', message: errorMessage },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
if (!args.prompt) {
|
||||
const errorMessage =
|
||||
'No prompt specified. Please provide a prompt with new information for the task update.';
|
||||
log.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'MISSING_PROMPT', message: errorMessage },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Parse taskId - handle both string and number values
|
||||
let taskId;
|
||||
if (typeof args.id === 'string') {
|
||||
// Handle subtask IDs (e.g., "5.2")
|
||||
if (args.id.includes('.')) {
|
||||
taskId = args.id; // Keep as string for subtask IDs
|
||||
} else {
|
||||
// Parse as integer for main task IDs
|
||||
taskId = parseInt(args.id, 10);
|
||||
if (isNaN(taskId)) {
|
||||
const errorMessage = `Invalid task ID: ${args.id}. Task ID must be a positive integer or subtask ID (e.g., "5.2").`;
|
||||
log.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'INVALID_TASK_ID', message: errorMessage },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
}
|
||||
} else {
|
||||
taskId = args.id;
|
||||
}
|
||||
|
||||
// Get tasks file path
|
||||
let tasksPath;
|
||||
try {
|
||||
tasksPath = findTasksJsonPath(args, log);
|
||||
} catch (error) {
|
||||
log.error(`Error finding tasks file: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'TASKS_FILE_ERROR', message: error.message },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Get research flag
|
||||
const useResearch = args.research === true;
|
||||
|
||||
// Initialize appropriate AI client based on research flag
|
||||
let aiClient;
|
||||
try {
|
||||
if (useResearch) {
|
||||
log.info('Using Perplexity AI for research-backed task update');
|
||||
aiClient = await getPerplexityClientForMCP(session, log);
|
||||
} else {
|
||||
log.info('Using Claude AI for task update');
|
||||
aiClient = getAnthropicClientForMCP(session, log);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`Failed to initialize AI client: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'AI_CLIENT_ERROR',
|
||||
message: `Cannot initialize AI client: ${error.message}`
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
log.info(
|
||||
`Updating task with ID ${taskId} with prompt "${args.prompt}" and research: ${useResearch}`
|
||||
);
|
||||
|
||||
try {
|
||||
// Enable silent mode to prevent console logs from interfering with JSON response
|
||||
enableSilentMode();
|
||||
|
||||
// Create a logger wrapper that matches what updateTaskById expects
|
||||
const logWrapper = {
|
||||
info: (message) => log.info(message),
|
||||
warn: (message) => log.warn(message),
|
||||
error: (message) => log.error(message),
|
||||
debug: (message) => log.debug && log.debug(message),
|
||||
success: (message) => log.info(message) // Map success to info since many loggers don't have success
|
||||
};
|
||||
|
||||
// Execute core updateTaskById function with proper parameters
|
||||
await updateTaskById(
|
||||
tasksPath,
|
||||
taskId,
|
||||
args.prompt,
|
||||
useResearch,
|
||||
{
|
||||
mcpLog: logWrapper, // Use our wrapper object that has the expected method structure
|
||||
session
|
||||
},
|
||||
'json'
|
||||
);
|
||||
|
||||
// Since updateTaskById doesn't return a value but modifies the tasks file,
|
||||
// we'll return a success message
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
message: `Successfully updated task with ID ${taskId} based on the prompt`,
|
||||
taskId,
|
||||
tasksPath,
|
||||
useResearch
|
||||
},
|
||||
fromCache: false // This operation always modifies state and should never be cached
|
||||
};
|
||||
} catch (error) {
|
||||
log.error(`Error updating task by ID: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'UPDATE_TASK_ERROR',
|
||||
message: error.message || 'Unknown error updating task'
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
} finally {
|
||||
// Make sure to restore normal logging even if there's an error
|
||||
disableSilentMode();
|
||||
}
|
||||
} catch (error) {
|
||||
// Ensure silent mode is disabled
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Error updating task by ID: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'UPDATE_TASK_ERROR',
|
||||
message: error.message || 'Unknown error updating task'
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,168 +4,177 @@
|
||||
*/
|
||||
|
||||
import { updateTasks } from '../../../../scripts/modules/task-manager.js';
|
||||
import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js';
|
||||
import {
|
||||
enableSilentMode,
|
||||
disableSilentMode
|
||||
} from '../../../../scripts/modules/utils.js';
|
||||
import { findTasksJsonPath } from '../utils/path-utils.js';
|
||||
import {
|
||||
getAnthropicClientForMCP,
|
||||
getPerplexityClientForMCP
|
||||
import {
|
||||
getAnthropicClientForMCP,
|
||||
getPerplexityClientForMCP
|
||||
} from '../utils/ai-client-utils.js';
|
||||
|
||||
/**
|
||||
* Direct function wrapper for updating tasks based on new context/prompt.
|
||||
*
|
||||
*
|
||||
* @param {Object} args - Command arguments containing fromId, prompt, useResearch and file path options.
|
||||
* @param {Object} log - Logger object.
|
||||
* @param {Object} context - Context object containing session data.
|
||||
* @returns {Promise<Object>} - Result object with success status and data/error information.
|
||||
*/
|
||||
export async function updateTasksDirect(args, log, context = {}) {
|
||||
const { session } = context; // Only extract session, not reportProgress
|
||||
|
||||
try {
|
||||
log.info(`Updating tasks with args: ${JSON.stringify(args)}`);
|
||||
|
||||
// Check for the common mistake of using 'id' instead of 'from'
|
||||
if (args.id !== undefined && args.from === undefined) {
|
||||
const errorMessage = "You specified 'id' parameter but 'update' requires 'from' parameter. Use 'from' for this tool or use 'update_task' tool if you want to update a single task.";
|
||||
log.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'PARAMETER_MISMATCH',
|
||||
message: errorMessage,
|
||||
suggestion: "Use 'from' parameter instead of 'id', or use the 'update_task' tool for single task updates"
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Check required parameters
|
||||
if (!args.from) {
|
||||
const errorMessage = 'No from ID specified. Please provide a task ID to start updating from.';
|
||||
log.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'MISSING_FROM_ID', message: errorMessage },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
if (!args.prompt) {
|
||||
const errorMessage = 'No prompt specified. Please provide a prompt with new context for task updates.';
|
||||
log.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'MISSING_PROMPT', message: errorMessage },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Parse fromId - handle both string and number values
|
||||
let fromId;
|
||||
if (typeof args.from === 'string') {
|
||||
fromId = parseInt(args.from, 10);
|
||||
if (isNaN(fromId)) {
|
||||
const errorMessage = `Invalid from ID: ${args.from}. Task ID must be a positive integer.`;
|
||||
log.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'INVALID_FROM_ID', message: errorMessage },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
} else {
|
||||
fromId = args.from;
|
||||
}
|
||||
|
||||
// Get tasks file path
|
||||
let tasksPath;
|
||||
try {
|
||||
tasksPath = findTasksJsonPath(args, log);
|
||||
} catch (error) {
|
||||
log.error(`Error finding tasks file: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'TASKS_FILE_ERROR', message: error.message },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Get research flag
|
||||
const useResearch = args.research === true;
|
||||
|
||||
// Initialize appropriate AI client based on research flag
|
||||
let aiClient;
|
||||
try {
|
||||
if (useResearch) {
|
||||
log.info('Using Perplexity AI for research-backed task updates');
|
||||
aiClient = await getPerplexityClientForMCP(session, log);
|
||||
} else {
|
||||
log.info('Using Claude AI for task updates');
|
||||
aiClient = getAnthropicClientForMCP(session, log);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`Failed to initialize AI client: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'AI_CLIENT_ERROR',
|
||||
message: `Cannot initialize AI client: ${error.message}`
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
log.info(`Updating tasks from ID ${fromId} with prompt "${args.prompt}" and research: ${useResearch}`);
|
||||
|
||||
try {
|
||||
// Enable silent mode to prevent console logs from interfering with JSON response
|
||||
enableSilentMode();
|
||||
|
||||
// Execute core updateTasks function, passing the AI client and session
|
||||
await updateTasks(
|
||||
tasksPath,
|
||||
fromId,
|
||||
args.prompt,
|
||||
useResearch,
|
||||
{
|
||||
mcpLog: log,
|
||||
session
|
||||
}
|
||||
);
|
||||
|
||||
// Since updateTasks doesn't return a value but modifies the tasks file,
|
||||
// we'll return a success message
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
message: `Successfully updated tasks from ID ${fromId} based on the prompt`,
|
||||
fromId,
|
||||
tasksPath,
|
||||
useResearch
|
||||
},
|
||||
fromCache: false // This operation always modifies state and should never be cached
|
||||
};
|
||||
} catch (error) {
|
||||
log.error(`Error updating tasks: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'UPDATE_TASKS_ERROR', message: error.message || 'Unknown error updating tasks' },
|
||||
fromCache: false
|
||||
};
|
||||
} finally {
|
||||
// Make sure to restore normal logging even if there's an error
|
||||
disableSilentMode();
|
||||
}
|
||||
} catch (error) {
|
||||
// Ensure silent mode is disabled
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Error updating tasks: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'UPDATE_TASKS_ERROR', message: error.message || 'Unknown error updating tasks' },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
}
|
||||
const { session } = context; // Only extract session, not reportProgress
|
||||
|
||||
try {
|
||||
log.info(`Updating tasks with args: ${JSON.stringify(args)}`);
|
||||
|
||||
// Check for the common mistake of using 'id' instead of 'from'
|
||||
if (args.id !== undefined && args.from === undefined) {
|
||||
const errorMessage =
|
||||
"You specified 'id' parameter but 'update' requires 'from' parameter. Use 'from' for this tool or use 'update_task' tool if you want to update a single task.";
|
||||
log.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'PARAMETER_MISMATCH',
|
||||
message: errorMessage,
|
||||
suggestion:
|
||||
"Use 'from' parameter instead of 'id', or use the 'update_task' tool for single task updates"
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Check required parameters
|
||||
if (!args.from) {
|
||||
const errorMessage =
|
||||
'No from ID specified. Please provide a task ID to start updating from.';
|
||||
log.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'MISSING_FROM_ID', message: errorMessage },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
if (!args.prompt) {
|
||||
const errorMessage =
|
||||
'No prompt specified. Please provide a prompt with new context for task updates.';
|
||||
log.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'MISSING_PROMPT', message: errorMessage },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Parse fromId - handle both string and number values
|
||||
let fromId;
|
||||
if (typeof args.from === 'string') {
|
||||
fromId = parseInt(args.from, 10);
|
||||
if (isNaN(fromId)) {
|
||||
const errorMessage = `Invalid from ID: ${args.from}. Task ID must be a positive integer.`;
|
||||
log.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'INVALID_FROM_ID', message: errorMessage },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
} else {
|
||||
fromId = args.from;
|
||||
}
|
||||
|
||||
// Get tasks file path
|
||||
let tasksPath;
|
||||
try {
|
||||
tasksPath = findTasksJsonPath(args, log);
|
||||
} catch (error) {
|
||||
log.error(`Error finding tasks file: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'TASKS_FILE_ERROR', message: error.message },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Get research flag
|
||||
const useResearch = args.research === true;
|
||||
|
||||
// Initialize appropriate AI client based on research flag
|
||||
let aiClient;
|
||||
try {
|
||||
if (useResearch) {
|
||||
log.info('Using Perplexity AI for research-backed task updates');
|
||||
aiClient = await getPerplexityClientForMCP(session, log);
|
||||
} else {
|
||||
log.info('Using Claude AI for task updates');
|
||||
aiClient = getAnthropicClientForMCP(session, log);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`Failed to initialize AI client: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'AI_CLIENT_ERROR',
|
||||
message: `Cannot initialize AI client: ${error.message}`
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
log.info(
|
||||
`Updating tasks from ID ${fromId} with prompt "${args.prompt}" and research: ${useResearch}`
|
||||
);
|
||||
|
||||
try {
|
||||
// Enable silent mode to prevent console logs from interfering with JSON response
|
||||
enableSilentMode();
|
||||
|
||||
// Execute core updateTasks function, passing the AI client and session
|
||||
await updateTasks(tasksPath, fromId, args.prompt, useResearch, {
|
||||
mcpLog: log,
|
||||
session
|
||||
});
|
||||
|
||||
// Since updateTasks doesn't return a value but modifies the tasks file,
|
||||
// we'll return a success message
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
message: `Successfully updated tasks from ID ${fromId} based on the prompt`,
|
||||
fromId,
|
||||
tasksPath,
|
||||
useResearch
|
||||
},
|
||||
fromCache: false // This operation always modifies state and should never be cached
|
||||
};
|
||||
} catch (error) {
|
||||
log.error(`Error updating tasks: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'UPDATE_TASKS_ERROR',
|
||||
message: error.message || 'Unknown error updating tasks'
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
} finally {
|
||||
// Make sure to restore normal logging even if there's an error
|
||||
disableSilentMode();
|
||||
}
|
||||
} catch (error) {
|
||||
// Ensure silent mode is disabled
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Error updating tasks: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'UPDATE_TASKS_ERROR',
|
||||
message: error.message || 'Unknown error updating tasks'
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
|
||||
import { validateDependenciesCommand } from '../../../../scripts/modules/dependency-manager.js';
|
||||
import { findTasksJsonPath } from '../utils/path-utils.js';
|
||||
import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js';
|
||||
import {
|
||||
enableSilentMode,
|
||||
disableSilentMode
|
||||
} from '../../../../scripts/modules/utils.js';
|
||||
import fs from 'fs';
|
||||
|
||||
/**
|
||||
@@ -16,50 +19,50 @@ import fs from 'fs';
|
||||
* @returns {Promise<{success: boolean, data?: Object, error?: {code: string, message: string}}>}
|
||||
*/
|
||||
export async function validateDependenciesDirect(args, log) {
|
||||
try {
|
||||
log.info(`Validating dependencies in tasks...`);
|
||||
|
||||
// Find the tasks.json path
|
||||
const tasksPath = findTasksJsonPath(args, log);
|
||||
|
||||
// Verify the file exists
|
||||
if (!fs.existsSync(tasksPath)) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'FILE_NOT_FOUND',
|
||||
message: `Tasks file not found at ${tasksPath}`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Enable silent mode to prevent console logs from interfering with JSON response
|
||||
enableSilentMode();
|
||||
|
||||
// Call the original command function
|
||||
await validateDependenciesCommand(tasksPath);
|
||||
|
||||
// Restore normal logging
|
||||
disableSilentMode();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
message: 'Dependencies validated successfully',
|
||||
tasksPath
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
// Make sure to restore normal logging even if there's an error
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Error validating dependencies: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'VALIDATION_ERROR',
|
||||
message: error.message
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
try {
|
||||
log.info(`Validating dependencies in tasks...`);
|
||||
|
||||
// Find the tasks.json path
|
||||
const tasksPath = findTasksJsonPath(args, log);
|
||||
|
||||
// Verify the file exists
|
||||
if (!fs.existsSync(tasksPath)) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'FILE_NOT_FOUND',
|
||||
message: `Tasks file not found at ${tasksPath}`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Enable silent mode to prevent console logs from interfering with JSON response
|
||||
enableSilentMode();
|
||||
|
||||
// Call the original command function
|
||||
await validateDependenciesCommand(tasksPath);
|
||||
|
||||
// Restore normal logging
|
||||
disableSilentMode();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
message: 'Dependencies validated successfully',
|
||||
tasksPath
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
// Make sure to restore normal logging even if there's an error
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Error validating dependencies: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'VALIDATION_ERROR',
|
||||
message: error.message
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,64 +33,64 @@ import { removeTaskDirect } from './direct-functions/remove-task.js';
|
||||
export { findTasksJsonPath } from './utils/path-utils.js';
|
||||
|
||||
// Re-export AI client utilities
|
||||
export {
|
||||
getAnthropicClientForMCP,
|
||||
getPerplexityClientForMCP,
|
||||
getModelConfig,
|
||||
getBestAvailableAIModel,
|
||||
handleClaudeError
|
||||
export {
|
||||
getAnthropicClientForMCP,
|
||||
getPerplexityClientForMCP,
|
||||
getModelConfig,
|
||||
getBestAvailableAIModel,
|
||||
handleClaudeError
|
||||
} from './utils/ai-client-utils.js';
|
||||
|
||||
// Use Map for potential future enhancements like introspection or dynamic dispatch
|
||||
export const directFunctions = new Map([
|
||||
['listTasksDirect', listTasksDirect],
|
||||
['getCacheStatsDirect', getCacheStatsDirect],
|
||||
['parsePRDDirect', parsePRDDirect],
|
||||
['updateTasksDirect', updateTasksDirect],
|
||||
['updateTaskByIdDirect', updateTaskByIdDirect],
|
||||
['updateSubtaskByIdDirect', updateSubtaskByIdDirect],
|
||||
['generateTaskFilesDirect', generateTaskFilesDirect],
|
||||
['setTaskStatusDirect', setTaskStatusDirect],
|
||||
['showTaskDirect', showTaskDirect],
|
||||
['nextTaskDirect', nextTaskDirect],
|
||||
['expandTaskDirect', expandTaskDirect],
|
||||
['addTaskDirect', addTaskDirect],
|
||||
['addSubtaskDirect', addSubtaskDirect],
|
||||
['removeSubtaskDirect', removeSubtaskDirect],
|
||||
['analyzeTaskComplexityDirect', analyzeTaskComplexityDirect],
|
||||
['clearSubtasksDirect', clearSubtasksDirect],
|
||||
['expandAllTasksDirect', expandAllTasksDirect],
|
||||
['removeDependencyDirect', removeDependencyDirect],
|
||||
['validateDependenciesDirect', validateDependenciesDirect],
|
||||
['fixDependenciesDirect', fixDependenciesDirect],
|
||||
['complexityReportDirect', complexityReportDirect],
|
||||
['addDependencyDirect', addDependencyDirect],
|
||||
['removeTaskDirect', removeTaskDirect]
|
||||
['listTasksDirect', listTasksDirect],
|
||||
['getCacheStatsDirect', getCacheStatsDirect],
|
||||
['parsePRDDirect', parsePRDDirect],
|
||||
['updateTasksDirect', updateTasksDirect],
|
||||
['updateTaskByIdDirect', updateTaskByIdDirect],
|
||||
['updateSubtaskByIdDirect', updateSubtaskByIdDirect],
|
||||
['generateTaskFilesDirect', generateTaskFilesDirect],
|
||||
['setTaskStatusDirect', setTaskStatusDirect],
|
||||
['showTaskDirect', showTaskDirect],
|
||||
['nextTaskDirect', nextTaskDirect],
|
||||
['expandTaskDirect', expandTaskDirect],
|
||||
['addTaskDirect', addTaskDirect],
|
||||
['addSubtaskDirect', addSubtaskDirect],
|
||||
['removeSubtaskDirect', removeSubtaskDirect],
|
||||
['analyzeTaskComplexityDirect', analyzeTaskComplexityDirect],
|
||||
['clearSubtasksDirect', clearSubtasksDirect],
|
||||
['expandAllTasksDirect', expandAllTasksDirect],
|
||||
['removeDependencyDirect', removeDependencyDirect],
|
||||
['validateDependenciesDirect', validateDependenciesDirect],
|
||||
['fixDependenciesDirect', fixDependenciesDirect],
|
||||
['complexityReportDirect', complexityReportDirect],
|
||||
['addDependencyDirect', addDependencyDirect],
|
||||
['removeTaskDirect', removeTaskDirect]
|
||||
]);
|
||||
|
||||
// Re-export all direct function implementations
|
||||
export {
|
||||
listTasksDirect,
|
||||
getCacheStatsDirect,
|
||||
parsePRDDirect,
|
||||
updateTasksDirect,
|
||||
updateTaskByIdDirect,
|
||||
updateSubtaskByIdDirect,
|
||||
generateTaskFilesDirect,
|
||||
setTaskStatusDirect,
|
||||
showTaskDirect,
|
||||
nextTaskDirect,
|
||||
expandTaskDirect,
|
||||
addTaskDirect,
|
||||
addSubtaskDirect,
|
||||
removeSubtaskDirect,
|
||||
analyzeTaskComplexityDirect,
|
||||
clearSubtasksDirect,
|
||||
expandAllTasksDirect,
|
||||
removeDependencyDirect,
|
||||
validateDependenciesDirect,
|
||||
fixDependenciesDirect,
|
||||
complexityReportDirect,
|
||||
addDependencyDirect,
|
||||
removeTaskDirect
|
||||
};
|
||||
listTasksDirect,
|
||||
getCacheStatsDirect,
|
||||
parsePRDDirect,
|
||||
updateTasksDirect,
|
||||
updateTaskByIdDirect,
|
||||
updateSubtaskByIdDirect,
|
||||
generateTaskFilesDirect,
|
||||
setTaskStatusDirect,
|
||||
showTaskDirect,
|
||||
nextTaskDirect,
|
||||
expandTaskDirect,
|
||||
addTaskDirect,
|
||||
addSubtaskDirect,
|
||||
removeSubtaskDirect,
|
||||
analyzeTaskComplexityDirect,
|
||||
clearSubtasksDirect,
|
||||
expandAllTasksDirect,
|
||||
removeDependencyDirect,
|
||||
validateDependenciesDirect,
|
||||
fixDependenciesDirect,
|
||||
complexityReportDirect,
|
||||
addDependencyDirect,
|
||||
removeTaskDirect
|
||||
};
|
||||
|
||||
@@ -11,9 +11,9 @@ dotenv.config();
|
||||
|
||||
// Default model configuration from CLI environment
|
||||
const DEFAULT_MODEL_CONFIG = {
|
||||
model: 'claude-3-7-sonnet-20250219',
|
||||
maxTokens: 64000,
|
||||
temperature: 0.2
|
||||
model: 'claude-3-7-sonnet-20250219',
|
||||
maxTokens: 64000,
|
||||
temperature: 0.2
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -24,25 +24,28 @@ const DEFAULT_MODEL_CONFIG = {
|
||||
* @throws {Error} If API key is missing
|
||||
*/
|
||||
export function getAnthropicClientForMCP(session, log = console) {
|
||||
try {
|
||||
// Extract API key from session.env or fall back to environment variables
|
||||
const apiKey = session?.env?.ANTHROPIC_API_KEY || process.env.ANTHROPIC_API_KEY;
|
||||
|
||||
if (!apiKey) {
|
||||
throw new Error('ANTHROPIC_API_KEY not found in session environment or process.env');
|
||||
}
|
||||
|
||||
// Initialize and return a new Anthropic client
|
||||
return new Anthropic({
|
||||
apiKey,
|
||||
defaultHeaders: {
|
||||
'anthropic-beta': 'output-128k-2025-02-19' // Include header for increased token limit
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
log.error(`Failed to initialize Anthropic client: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
try {
|
||||
// Extract API key from session.env or fall back to environment variables
|
||||
const apiKey =
|
||||
session?.env?.ANTHROPIC_API_KEY || process.env.ANTHROPIC_API_KEY;
|
||||
|
||||
if (!apiKey) {
|
||||
throw new Error(
|
||||
'ANTHROPIC_API_KEY not found in session environment or process.env'
|
||||
);
|
||||
}
|
||||
|
||||
// Initialize and return a new Anthropic client
|
||||
return new Anthropic({
|
||||
apiKey,
|
||||
defaultHeaders: {
|
||||
'anthropic-beta': 'output-128k-2025-02-19' // Include header for increased token limit
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
log.error(`Failed to initialize Anthropic client: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,26 +56,29 @@ export function getAnthropicClientForMCP(session, log = console) {
|
||||
* @throws {Error} If API key is missing or OpenAI package can't be imported
|
||||
*/
|
||||
export async function getPerplexityClientForMCP(session, log = console) {
|
||||
try {
|
||||
// Extract API key from session.env or fall back to environment variables
|
||||
const apiKey = session?.env?.PERPLEXITY_API_KEY || process.env.PERPLEXITY_API_KEY;
|
||||
|
||||
if (!apiKey) {
|
||||
throw new Error('PERPLEXITY_API_KEY not found in session environment or process.env');
|
||||
}
|
||||
|
||||
// Dynamically import OpenAI (it may not be used in all contexts)
|
||||
const { default: OpenAI } = await import('openai');
|
||||
|
||||
// Initialize and return a new OpenAI client configured for Perplexity
|
||||
return new OpenAI({
|
||||
apiKey,
|
||||
baseURL: 'https://api.perplexity.ai'
|
||||
});
|
||||
} catch (error) {
|
||||
log.error(`Failed to initialize Perplexity client: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
try {
|
||||
// Extract API key from session.env or fall back to environment variables
|
||||
const apiKey =
|
||||
session?.env?.PERPLEXITY_API_KEY || process.env.PERPLEXITY_API_KEY;
|
||||
|
||||
if (!apiKey) {
|
||||
throw new Error(
|
||||
'PERPLEXITY_API_KEY not found in session environment or process.env'
|
||||
);
|
||||
}
|
||||
|
||||
// Dynamically import OpenAI (it may not be used in all contexts)
|
||||
const { default: OpenAI } = await import('openai');
|
||||
|
||||
// Initialize and return a new OpenAI client configured for Perplexity
|
||||
return new OpenAI({
|
||||
apiKey,
|
||||
baseURL: 'https://api.perplexity.ai'
|
||||
});
|
||||
} catch (error) {
|
||||
log.error(`Failed to initialize Perplexity client: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -82,12 +88,12 @@ export async function getPerplexityClientForMCP(session, log = console) {
|
||||
* @returns {Object} Model configuration with model, maxTokens, and temperature
|
||||
*/
|
||||
export function getModelConfig(session, defaults = DEFAULT_MODEL_CONFIG) {
|
||||
// Get values from session or fall back to defaults
|
||||
return {
|
||||
model: session?.env?.MODEL || defaults.model,
|
||||
maxTokens: parseInt(session?.env?.MAX_TOKENS || defaults.maxTokens),
|
||||
temperature: parseFloat(session?.env?.TEMPERATURE || defaults.temperature)
|
||||
};
|
||||
// Get values from session or fall back to defaults
|
||||
return {
|
||||
model: session?.env?.MODEL || defaults.model,
|
||||
maxTokens: parseInt(session?.env?.MAX_TOKENS || defaults.maxTokens),
|
||||
temperature: parseFloat(session?.env?.TEMPERATURE || defaults.temperature)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,59 +106,78 @@ export function getModelConfig(session, defaults = DEFAULT_MODEL_CONFIG) {
|
||||
* @returns {Promise<Object>} Selected model info with type and client
|
||||
* @throws {Error} If no AI models are available
|
||||
*/
|
||||
export async function getBestAvailableAIModel(session, options = {}, log = console) {
|
||||
const { requiresResearch = false, claudeOverloaded = false } = options;
|
||||
|
||||
// Test case: When research is needed but no Perplexity, use Claude
|
||||
if (requiresResearch &&
|
||||
!(session?.env?.PERPLEXITY_API_KEY || process.env.PERPLEXITY_API_KEY) &&
|
||||
(session?.env?.ANTHROPIC_API_KEY || process.env.ANTHROPIC_API_KEY)) {
|
||||
try {
|
||||
log.warn('Perplexity not available for research, using Claude');
|
||||
const client = getAnthropicClientForMCP(session, log);
|
||||
return { type: 'claude', client };
|
||||
} catch (error) {
|
||||
log.error(`Claude not available: ${error.message}`);
|
||||
throw new Error('No AI models available for research');
|
||||
}
|
||||
}
|
||||
|
||||
// Regular path: Perplexity for research when available
|
||||
if (requiresResearch && (session?.env?.PERPLEXITY_API_KEY || process.env.PERPLEXITY_API_KEY)) {
|
||||
try {
|
||||
const client = await getPerplexityClientForMCP(session, log);
|
||||
return { type: 'perplexity', client };
|
||||
} catch (error) {
|
||||
log.warn(`Perplexity not available: ${error.message}`);
|
||||
// Fall through to Claude as backup
|
||||
}
|
||||
}
|
||||
|
||||
// Test case: Claude for overloaded scenario
|
||||
if (claudeOverloaded && (session?.env?.ANTHROPIC_API_KEY || process.env.ANTHROPIC_API_KEY)) {
|
||||
try {
|
||||
log.warn('Claude is overloaded but no alternatives are available. Proceeding with Claude anyway.');
|
||||
const client = getAnthropicClientForMCP(session, log);
|
||||
return { type: 'claude', client };
|
||||
} catch (error) {
|
||||
log.error(`Claude not available despite being overloaded: ${error.message}`);
|
||||
throw new Error('No AI models available');
|
||||
}
|
||||
}
|
||||
|
||||
// Default case: Use Claude when available and not overloaded
|
||||
if (!claudeOverloaded && (session?.env?.ANTHROPIC_API_KEY || process.env.ANTHROPIC_API_KEY)) {
|
||||
try {
|
||||
const client = getAnthropicClientForMCP(session, log);
|
||||
return { type: 'claude', client };
|
||||
} catch (error) {
|
||||
log.warn(`Claude not available: ${error.message}`);
|
||||
// Fall through to error if no other options
|
||||
}
|
||||
}
|
||||
|
||||
// If we got here, no models were successfully initialized
|
||||
throw new Error('No AI models available. Please check your API keys.');
|
||||
export async function getBestAvailableAIModel(
|
||||
session,
|
||||
options = {},
|
||||
log = console
|
||||
) {
|
||||
const { requiresResearch = false, claudeOverloaded = false } = options;
|
||||
|
||||
// Test case: When research is needed but no Perplexity, use Claude
|
||||
if (
|
||||
requiresResearch &&
|
||||
!(session?.env?.PERPLEXITY_API_KEY || process.env.PERPLEXITY_API_KEY) &&
|
||||
(session?.env?.ANTHROPIC_API_KEY || process.env.ANTHROPIC_API_KEY)
|
||||
) {
|
||||
try {
|
||||
log.warn('Perplexity not available for research, using Claude');
|
||||
const client = getAnthropicClientForMCP(session, log);
|
||||
return { type: 'claude', client };
|
||||
} catch (error) {
|
||||
log.error(`Claude not available: ${error.message}`);
|
||||
throw new Error('No AI models available for research');
|
||||
}
|
||||
}
|
||||
|
||||
// Regular path: Perplexity for research when available
|
||||
if (
|
||||
requiresResearch &&
|
||||
(session?.env?.PERPLEXITY_API_KEY || process.env.PERPLEXITY_API_KEY)
|
||||
) {
|
||||
try {
|
||||
const client = await getPerplexityClientForMCP(session, log);
|
||||
return { type: 'perplexity', client };
|
||||
} catch (error) {
|
||||
log.warn(`Perplexity not available: ${error.message}`);
|
||||
// Fall through to Claude as backup
|
||||
}
|
||||
}
|
||||
|
||||
// Test case: Claude for overloaded scenario
|
||||
if (
|
||||
claudeOverloaded &&
|
||||
(session?.env?.ANTHROPIC_API_KEY || process.env.ANTHROPIC_API_KEY)
|
||||
) {
|
||||
try {
|
||||
log.warn(
|
||||
'Claude is overloaded but no alternatives are available. Proceeding with Claude anyway.'
|
||||
);
|
||||
const client = getAnthropicClientForMCP(session, log);
|
||||
return { type: 'claude', client };
|
||||
} catch (error) {
|
||||
log.error(
|
||||
`Claude not available despite being overloaded: ${error.message}`
|
||||
);
|
||||
throw new Error('No AI models available');
|
||||
}
|
||||
}
|
||||
|
||||
// Default case: Use Claude when available and not overloaded
|
||||
if (
|
||||
!claudeOverloaded &&
|
||||
(session?.env?.ANTHROPIC_API_KEY || process.env.ANTHROPIC_API_KEY)
|
||||
) {
|
||||
try {
|
||||
const client = getAnthropicClientForMCP(session, log);
|
||||
return { type: 'claude', client };
|
||||
} catch (error) {
|
||||
log.warn(`Claude not available: ${error.message}`);
|
||||
// Fall through to error if no other options
|
||||
}
|
||||
}
|
||||
|
||||
// If we got here, no models were successfully initialized
|
||||
throw new Error('No AI models available. Please check your API keys.');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -161,28 +186,28 @@ export async function getBestAvailableAIModel(session, options = {}, log = conso
|
||||
* @returns {string} User-friendly error message
|
||||
*/
|
||||
export function handleClaudeError(error) {
|
||||
// Check if it's a structured error response
|
||||
if (error.type === 'error' && error.error) {
|
||||
switch (error.error.type) {
|
||||
case 'overloaded_error':
|
||||
return 'Claude is currently experiencing high demand and is overloaded. Please wait a few minutes and try again.';
|
||||
case 'rate_limit_error':
|
||||
return 'You have exceeded the rate limit. Please wait a few minutes before making more requests.';
|
||||
case 'invalid_request_error':
|
||||
return 'There was an issue with the request format. If this persists, please report it as a bug.';
|
||||
default:
|
||||
return `Claude API error: ${error.error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for network/timeout errors
|
||||
if (error.message?.toLowerCase().includes('timeout')) {
|
||||
return 'The request to Claude timed out. Please try again.';
|
||||
}
|
||||
if (error.message?.toLowerCase().includes('network')) {
|
||||
return 'There was a network error connecting to Claude. Please check your internet connection and try again.';
|
||||
}
|
||||
|
||||
// Default error message
|
||||
return `Error communicating with Claude: ${error.message}`;
|
||||
}
|
||||
// Check if it's a structured error response
|
||||
if (error.type === 'error' && error.error) {
|
||||
switch (error.error.type) {
|
||||
case 'overloaded_error':
|
||||
return 'Claude is currently experiencing high demand and is overloaded. Please wait a few minutes and try again.';
|
||||
case 'rate_limit_error':
|
||||
return 'You have exceeded the rate limit. Please wait a few minutes before making more requests.';
|
||||
case 'invalid_request_error':
|
||||
return 'There was an issue with the request format. If this persists, please report it as a bug.';
|
||||
default:
|
||||
return `Claude API error: ${error.error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for network/timeout errors
|
||||
if (error.message?.toLowerCase().includes('timeout')) {
|
||||
return 'The request to Claude timed out. Please try again.';
|
||||
}
|
||||
if (error.message?.toLowerCase().includes('network')) {
|
||||
return 'There was a network error connecting to Claude. Please check your internet connection and try again.';
|
||||
}
|
||||
|
||||
// Default error message
|
||||
return `Error communicating with Claude: ${error.message}`;
|
||||
}
|
||||
|
||||
@@ -1,213 +1,247 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
class AsyncOperationManager {
|
||||
constructor() {
|
||||
this.operations = new Map(); // Stores active operation state
|
||||
this.completedOperations = new Map(); // Stores completed operations
|
||||
this.maxCompletedOperations = 100; // Maximum number of completed operations to store
|
||||
this.listeners = new Map(); // For potential future notifications
|
||||
}
|
||||
constructor() {
|
||||
this.operations = new Map(); // Stores active operation state
|
||||
this.completedOperations = new Map(); // Stores completed operations
|
||||
this.maxCompletedOperations = 100; // Maximum number of completed operations to store
|
||||
this.listeners = new Map(); // For potential future notifications
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an operation to be executed asynchronously.
|
||||
* @param {Function} operationFn - The async function to execute (e.g., a Direct function).
|
||||
* @param {Object} args - Arguments to pass to the operationFn.
|
||||
* @param {Object} context - The MCP tool context { log, reportProgress, session }.
|
||||
* @returns {string} The unique ID assigned to this operation.
|
||||
*/
|
||||
addOperation(operationFn, args, context) {
|
||||
const operationId = `op-${uuidv4()}`;
|
||||
const operation = {
|
||||
id: operationId,
|
||||
status: 'pending',
|
||||
startTime: Date.now(),
|
||||
endTime: null,
|
||||
result: null,
|
||||
error: null,
|
||||
// Store necessary parts of context, especially log for background execution
|
||||
log: context.log,
|
||||
reportProgress: context.reportProgress, // Pass reportProgress through
|
||||
session: context.session // Pass session through if needed by the operationFn
|
||||
};
|
||||
this.operations.set(operationId, operation);
|
||||
this.log(operationId, 'info', `Operation added.`);
|
||||
/**
|
||||
* Adds an operation to be executed asynchronously.
|
||||
* @param {Function} operationFn - The async function to execute (e.g., a Direct function).
|
||||
* @param {Object} args - Arguments to pass to the operationFn.
|
||||
* @param {Object} context - The MCP tool context { log, reportProgress, session }.
|
||||
* @returns {string} The unique ID assigned to this operation.
|
||||
*/
|
||||
addOperation(operationFn, args, context) {
|
||||
const operationId = `op-${uuidv4()}`;
|
||||
const operation = {
|
||||
id: operationId,
|
||||
status: 'pending',
|
||||
startTime: Date.now(),
|
||||
endTime: null,
|
||||
result: null,
|
||||
error: null,
|
||||
// Store necessary parts of context, especially log for background execution
|
||||
log: context.log,
|
||||
reportProgress: context.reportProgress, // Pass reportProgress through
|
||||
session: context.session // Pass session through if needed by the operationFn
|
||||
};
|
||||
this.operations.set(operationId, operation);
|
||||
this.log(operationId, 'info', `Operation added.`);
|
||||
|
||||
// Start execution in the background (don't await here)
|
||||
this._runOperation(operationId, operationFn, args, context).catch(err => {
|
||||
// Catch unexpected errors during the async execution setup itself
|
||||
this.log(operationId, 'error', `Critical error starting operation: ${err.message}`, { stack: err.stack });
|
||||
operation.status = 'failed';
|
||||
operation.error = { code: 'MANAGER_EXECUTION_ERROR', message: err.message };
|
||||
operation.endTime = Date.now();
|
||||
|
||||
// Move to completed operations
|
||||
this._moveToCompleted(operationId);
|
||||
});
|
||||
// Start execution in the background (don't await here)
|
||||
this._runOperation(operationId, operationFn, args, context).catch((err) => {
|
||||
// Catch unexpected errors during the async execution setup itself
|
||||
this.log(
|
||||
operationId,
|
||||
'error',
|
||||
`Critical error starting operation: ${err.message}`,
|
||||
{ stack: err.stack }
|
||||
);
|
||||
operation.status = 'failed';
|
||||
operation.error = {
|
||||
code: 'MANAGER_EXECUTION_ERROR',
|
||||
message: err.message
|
||||
};
|
||||
operation.endTime = Date.now();
|
||||
|
||||
return operationId;
|
||||
}
|
||||
// Move to completed operations
|
||||
this._moveToCompleted(operationId);
|
||||
});
|
||||
|
||||
/**
|
||||
* Internal function to execute the operation.
|
||||
* @param {string} operationId - The ID of the operation.
|
||||
* @param {Function} operationFn - The async function to execute.
|
||||
* @param {Object} args - Arguments for the function.
|
||||
* @param {Object} context - The original MCP tool context.
|
||||
*/
|
||||
async _runOperation(operationId, operationFn, args, context) {
|
||||
const operation = this.operations.get(operationId);
|
||||
if (!operation) return; // Should not happen
|
||||
return operationId;
|
||||
}
|
||||
|
||||
operation.status = 'running';
|
||||
this.log(operationId, 'info', `Operation running.`);
|
||||
this.emit('statusChanged', { operationId, status: 'running' });
|
||||
/**
|
||||
* Internal function to execute the operation.
|
||||
* @param {string} operationId - The ID of the operation.
|
||||
* @param {Function} operationFn - The async function to execute.
|
||||
* @param {Object} args - Arguments for the function.
|
||||
* @param {Object} context - The original MCP tool context.
|
||||
*/
|
||||
async _runOperation(operationId, operationFn, args, context) {
|
||||
const operation = this.operations.get(operationId);
|
||||
if (!operation) return; // Should not happen
|
||||
|
||||
try {
|
||||
// Pass the necessary context parts to the direct function
|
||||
// The direct function needs to be adapted if it needs reportProgress
|
||||
// We pass the original context's log, plus our wrapped reportProgress
|
||||
const result = await operationFn(args, operation.log, {
|
||||
reportProgress: (progress) => this._handleProgress(operationId, progress),
|
||||
mcpLog: operation.log, // Pass log as mcpLog if direct fn expects it
|
||||
session: operation.session
|
||||
});
|
||||
|
||||
operation.status = result.success ? 'completed' : 'failed';
|
||||
operation.result = result.success ? result.data : null;
|
||||
operation.error = result.success ? null : result.error;
|
||||
this.log(operationId, 'info', `Operation finished with status: ${operation.status}`);
|
||||
operation.status = 'running';
|
||||
this.log(operationId, 'info', `Operation running.`);
|
||||
this.emit('statusChanged', { operationId, status: 'running' });
|
||||
|
||||
} catch (error) {
|
||||
this.log(operationId, 'error', `Operation failed with error: ${error.message}`, { stack: error.stack });
|
||||
operation.status = 'failed';
|
||||
operation.error = { code: 'OPERATION_EXECUTION_ERROR', message: error.message };
|
||||
} finally {
|
||||
operation.endTime = Date.now();
|
||||
this.emit('statusChanged', { operationId, status: operation.status, result: operation.result, error: operation.error });
|
||||
|
||||
// Move to completed operations if done or failed
|
||||
if (operation.status === 'completed' || operation.status === 'failed') {
|
||||
this._moveToCompleted(operationId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move an operation from active operations to completed operations history.
|
||||
* @param {string} operationId - The ID of the operation to move.
|
||||
* @private
|
||||
*/
|
||||
_moveToCompleted(operationId) {
|
||||
const operation = this.operations.get(operationId);
|
||||
if (!operation) return;
|
||||
|
||||
// Store only the necessary data in completed operations
|
||||
const completedData = {
|
||||
id: operation.id,
|
||||
status: operation.status,
|
||||
startTime: operation.startTime,
|
||||
endTime: operation.endTime,
|
||||
result: operation.result,
|
||||
error: operation.error,
|
||||
};
|
||||
|
||||
this.completedOperations.set(operationId, completedData);
|
||||
this.operations.delete(operationId);
|
||||
|
||||
// Trim completed operations if exceeding maximum
|
||||
if (this.completedOperations.size > this.maxCompletedOperations) {
|
||||
// Get the oldest operation (sorted by endTime)
|
||||
const oldest = [...this.completedOperations.entries()]
|
||||
.sort((a, b) => a[1].endTime - b[1].endTime)[0];
|
||||
|
||||
if (oldest) {
|
||||
this.completedOperations.delete(oldest[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles progress updates from the running operation and forwards them.
|
||||
* @param {string} operationId - The ID of the operation reporting progress.
|
||||
* @param {Object} progress - The progress object { progress, total? }.
|
||||
*/
|
||||
_handleProgress(operationId, progress) {
|
||||
const operation = this.operations.get(operationId);
|
||||
if (operation && operation.reportProgress) {
|
||||
try {
|
||||
// Use the reportProgress function captured from the original context
|
||||
operation.reportProgress(progress);
|
||||
this.log(operationId, 'debug', `Reported progress: ${JSON.stringify(progress)}`);
|
||||
} catch(err) {
|
||||
this.log(operationId, 'warn', `Failed to report progress: ${err.message}`);
|
||||
// Don't stop the operation, just log the reporting failure
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
// Pass the necessary context parts to the direct function
|
||||
// The direct function needs to be adapted if it needs reportProgress
|
||||
// We pass the original context's log, plus our wrapped reportProgress
|
||||
const result = await operationFn(args, operation.log, {
|
||||
reportProgress: (progress) =>
|
||||
this._handleProgress(operationId, progress),
|
||||
mcpLog: operation.log, // Pass log as mcpLog if direct fn expects it
|
||||
session: operation.session
|
||||
});
|
||||
|
||||
/**
|
||||
* Retrieves the status and result/error of an operation.
|
||||
* @param {string} operationId - The ID of the operation.
|
||||
* @returns {Object | null} The operation details or null if not found.
|
||||
*/
|
||||
getStatus(operationId) {
|
||||
// First check active operations
|
||||
const operation = this.operations.get(operationId);
|
||||
if (operation) {
|
||||
return {
|
||||
id: operation.id,
|
||||
status: operation.status,
|
||||
startTime: operation.startTime,
|
||||
endTime: operation.endTime,
|
||||
result: operation.result,
|
||||
error: operation.error,
|
||||
};
|
||||
}
|
||||
|
||||
// Then check completed operations
|
||||
const completedOperation = this.completedOperations.get(operationId);
|
||||
if (completedOperation) {
|
||||
return completedOperation;
|
||||
}
|
||||
|
||||
// Operation not found in either active or completed
|
||||
return {
|
||||
error: {
|
||||
code: 'OPERATION_NOT_FOUND',
|
||||
message: `Operation ID ${operationId} not found. It may have been completed and removed from history, or the ID may be invalid.`
|
||||
},
|
||||
status: 'not_found'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal logging helper to prefix logs with the operation ID.
|
||||
* @param {string} operationId - The ID of the operation.
|
||||
* @param {'info'|'warn'|'error'|'debug'} level - Log level.
|
||||
* @param {string} message - Log message.
|
||||
* @param {Object} [meta] - Additional metadata.
|
||||
*/
|
||||
log(operationId, level, message, meta = {}) {
|
||||
const operation = this.operations.get(operationId);
|
||||
// Use the logger instance associated with the operation if available, otherwise console
|
||||
const logger = operation?.log || console;
|
||||
const logFn = logger[level] || logger.log || console.log; // Fallback
|
||||
logFn(`[AsyncOp ${operationId}] ${message}`, meta);
|
||||
}
|
||||
operation.status = result.success ? 'completed' : 'failed';
|
||||
operation.result = result.success ? result.data : null;
|
||||
operation.error = result.success ? null : result.error;
|
||||
this.log(
|
||||
operationId,
|
||||
'info',
|
||||
`Operation finished with status: ${operation.status}`
|
||||
);
|
||||
} catch (error) {
|
||||
this.log(
|
||||
operationId,
|
||||
'error',
|
||||
`Operation failed with error: ${error.message}`,
|
||||
{ stack: error.stack }
|
||||
);
|
||||
operation.status = 'failed';
|
||||
operation.error = {
|
||||
code: 'OPERATION_EXECUTION_ERROR',
|
||||
message: error.message
|
||||
};
|
||||
} finally {
|
||||
operation.endTime = Date.now();
|
||||
this.emit('statusChanged', {
|
||||
operationId,
|
||||
status: operation.status,
|
||||
result: operation.result,
|
||||
error: operation.error
|
||||
});
|
||||
|
||||
// --- Basic Event Emitter ---
|
||||
on(eventName, listener) {
|
||||
if (!this.listeners.has(eventName)) {
|
||||
this.listeners.set(eventName, []);
|
||||
}
|
||||
this.listeners.get(eventName).push(listener);
|
||||
}
|
||||
// Move to completed operations if done or failed
|
||||
if (operation.status === 'completed' || operation.status === 'failed') {
|
||||
this._moveToCompleted(operationId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emit(eventName, data) {
|
||||
if (this.listeners.has(eventName)) {
|
||||
this.listeners.get(eventName).forEach(listener => listener(data));
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Move an operation from active operations to completed operations history.
|
||||
* @param {string} operationId - The ID of the operation to move.
|
||||
* @private
|
||||
*/
|
||||
_moveToCompleted(operationId) {
|
||||
const operation = this.operations.get(operationId);
|
||||
if (!operation) return;
|
||||
|
||||
// Store only the necessary data in completed operations
|
||||
const completedData = {
|
||||
id: operation.id,
|
||||
status: operation.status,
|
||||
startTime: operation.startTime,
|
||||
endTime: operation.endTime,
|
||||
result: operation.result,
|
||||
error: operation.error
|
||||
};
|
||||
|
||||
this.completedOperations.set(operationId, completedData);
|
||||
this.operations.delete(operationId);
|
||||
|
||||
// Trim completed operations if exceeding maximum
|
||||
if (this.completedOperations.size > this.maxCompletedOperations) {
|
||||
// Get the oldest operation (sorted by endTime)
|
||||
const oldest = [...this.completedOperations.entries()].sort(
|
||||
(a, b) => a[1].endTime - b[1].endTime
|
||||
)[0];
|
||||
|
||||
if (oldest) {
|
||||
this.completedOperations.delete(oldest[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles progress updates from the running operation and forwards them.
|
||||
* @param {string} operationId - The ID of the operation reporting progress.
|
||||
* @param {Object} progress - The progress object { progress, total? }.
|
||||
*/
|
||||
_handleProgress(operationId, progress) {
|
||||
const operation = this.operations.get(operationId);
|
||||
if (operation && operation.reportProgress) {
|
||||
try {
|
||||
// Use the reportProgress function captured from the original context
|
||||
operation.reportProgress(progress);
|
||||
this.log(
|
||||
operationId,
|
||||
'debug',
|
||||
`Reported progress: ${JSON.stringify(progress)}`
|
||||
);
|
||||
} catch (err) {
|
||||
this.log(
|
||||
operationId,
|
||||
'warn',
|
||||
`Failed to report progress: ${err.message}`
|
||||
);
|
||||
// Don't stop the operation, just log the reporting failure
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the status and result/error of an operation.
|
||||
* @param {string} operationId - The ID of the operation.
|
||||
* @returns {Object | null} The operation details or null if not found.
|
||||
*/
|
||||
getStatus(operationId) {
|
||||
// First check active operations
|
||||
const operation = this.operations.get(operationId);
|
||||
if (operation) {
|
||||
return {
|
||||
id: operation.id,
|
||||
status: operation.status,
|
||||
startTime: operation.startTime,
|
||||
endTime: operation.endTime,
|
||||
result: operation.result,
|
||||
error: operation.error
|
||||
};
|
||||
}
|
||||
|
||||
// Then check completed operations
|
||||
const completedOperation = this.completedOperations.get(operationId);
|
||||
if (completedOperation) {
|
||||
return completedOperation;
|
||||
}
|
||||
|
||||
// Operation not found in either active or completed
|
||||
return {
|
||||
error: {
|
||||
code: 'OPERATION_NOT_FOUND',
|
||||
message: `Operation ID ${operationId} not found. It may have been completed and removed from history, or the ID may be invalid.`
|
||||
},
|
||||
status: 'not_found'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal logging helper to prefix logs with the operation ID.
|
||||
* @param {string} operationId - The ID of the operation.
|
||||
* @param {'info'|'warn'|'error'|'debug'} level - Log level.
|
||||
* @param {string} message - Log message.
|
||||
* @param {Object} [meta] - Additional metadata.
|
||||
*/
|
||||
log(operationId, level, message, meta = {}) {
|
||||
const operation = this.operations.get(operationId);
|
||||
// Use the logger instance associated with the operation if available, otherwise console
|
||||
const logger = operation?.log || console;
|
||||
const logFn = logger[level] || logger.log || console.log; // Fallback
|
||||
logFn(`[AsyncOp ${operationId}] ${message}`, meta);
|
||||
}
|
||||
|
||||
// --- Basic Event Emitter ---
|
||||
on(eventName, listener) {
|
||||
if (!this.listeners.has(eventName)) {
|
||||
this.listeners.set(eventName, []);
|
||||
}
|
||||
this.listeners.get(eventName).push(listener);
|
||||
}
|
||||
|
||||
emit(eventName, data) {
|
||||
if (this.listeners.has(eventName)) {
|
||||
this.listeners.get(eventName).forEach((listener) => listener(data));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export a singleton instance
|
||||
|
||||
@@ -6,38 +6,42 @@
|
||||
* @returns {Promise<any>} The result of the actionFn.
|
||||
*/
|
||||
export async function withSessionEnv(sessionEnv, actionFn) {
|
||||
if (!sessionEnv || typeof sessionEnv !== 'object' || Object.keys(sessionEnv).length === 0) {
|
||||
// If no sessionEnv is provided, just run the action directly
|
||||
return await actionFn();
|
||||
}
|
||||
|
||||
const originalEnv = {};
|
||||
const keysToRestore = [];
|
||||
|
||||
// Set environment variables from sessionEnv
|
||||
for (const key in sessionEnv) {
|
||||
if (Object.prototype.hasOwnProperty.call(sessionEnv, key)) {
|
||||
// Store original value if it exists, otherwise mark for deletion
|
||||
if (process.env[key] !== undefined) {
|
||||
originalEnv[key] = process.env[key];
|
||||
}
|
||||
keysToRestore.push(key);
|
||||
process.env[key] = sessionEnv[key];
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Execute the provided action function
|
||||
return await actionFn();
|
||||
} finally {
|
||||
// Restore original environment variables
|
||||
for (const key of keysToRestore) {
|
||||
if (Object.prototype.hasOwnProperty.call(originalEnv, key)) {
|
||||
process.env[key] = originalEnv[key];
|
||||
} else {
|
||||
// If the key didn't exist originally, delete it
|
||||
delete process.env[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
!sessionEnv ||
|
||||
typeof sessionEnv !== 'object' ||
|
||||
Object.keys(sessionEnv).length === 0
|
||||
) {
|
||||
// If no sessionEnv is provided, just run the action directly
|
||||
return await actionFn();
|
||||
}
|
||||
|
||||
const originalEnv = {};
|
||||
const keysToRestore = [];
|
||||
|
||||
// Set environment variables from sessionEnv
|
||||
for (const key in sessionEnv) {
|
||||
if (Object.prototype.hasOwnProperty.call(sessionEnv, key)) {
|
||||
// Store original value if it exists, otherwise mark for deletion
|
||||
if (process.env[key] !== undefined) {
|
||||
originalEnv[key] = process.env[key];
|
||||
}
|
||||
keysToRestore.push(key);
|
||||
process.env[key] = sessionEnv[key];
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Execute the provided action function
|
||||
return await actionFn();
|
||||
} finally {
|
||||
// Restore original environment variables
|
||||
for (const key of keysToRestore) {
|
||||
if (Object.prototype.hasOwnProperty.call(originalEnv, key)) {
|
||||
process.env[key] = originalEnv[key];
|
||||
} else {
|
||||
// If the key didn't exist originally, delete it
|
||||
delete process.env[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/**
|
||||
* path-utils.js
|
||||
* Utility functions for file path operations in Task Master
|
||||
*
|
||||
*
|
||||
* This module provides robust path resolution for both:
|
||||
* 1. PACKAGE PATH: Where task-master code is installed
|
||||
* 1. PACKAGE PATH: Where task-master code is installed
|
||||
* (global node_modules OR local ./node_modules/task-master OR direct from repo)
|
||||
* 2. PROJECT PATH: Where user's tasks.json resides (typically user's project root)
|
||||
*/
|
||||
@@ -18,43 +18,43 @@ export let lastFoundProjectRoot = null;
|
||||
|
||||
// Project marker files that indicate a potential project root
|
||||
export const PROJECT_MARKERS = [
|
||||
// Task Master specific
|
||||
'tasks.json',
|
||||
'tasks/tasks.json',
|
||||
|
||||
// Common version control
|
||||
'.git',
|
||||
'.svn',
|
||||
|
||||
// Common package files
|
||||
'package.json',
|
||||
'pyproject.toml',
|
||||
'Gemfile',
|
||||
'go.mod',
|
||||
'Cargo.toml',
|
||||
|
||||
// Common IDE/editor folders
|
||||
'.cursor',
|
||||
'.vscode',
|
||||
'.idea',
|
||||
|
||||
// Common dependency directories (check if directory)
|
||||
'node_modules',
|
||||
'venv',
|
||||
'.venv',
|
||||
|
||||
// Common config files
|
||||
'.env',
|
||||
'.eslintrc',
|
||||
'tsconfig.json',
|
||||
'babel.config.js',
|
||||
'jest.config.js',
|
||||
'webpack.config.js',
|
||||
|
||||
// Common CI/CD files
|
||||
'.github/workflows',
|
||||
'.gitlab-ci.yml',
|
||||
'.circleci/config.yml'
|
||||
// Task Master specific
|
||||
'tasks.json',
|
||||
'tasks/tasks.json',
|
||||
|
||||
// Common version control
|
||||
'.git',
|
||||
'.svn',
|
||||
|
||||
// Common package files
|
||||
'package.json',
|
||||
'pyproject.toml',
|
||||
'Gemfile',
|
||||
'go.mod',
|
||||
'Cargo.toml',
|
||||
|
||||
// Common IDE/editor folders
|
||||
'.cursor',
|
||||
'.vscode',
|
||||
'.idea',
|
||||
|
||||
// Common dependency directories (check if directory)
|
||||
'node_modules',
|
||||
'venv',
|
||||
'.venv',
|
||||
|
||||
// Common config files
|
||||
'.env',
|
||||
'.eslintrc',
|
||||
'tsconfig.json',
|
||||
'babel.config.js',
|
||||
'jest.config.js',
|
||||
'webpack.config.js',
|
||||
|
||||
// Common CI/CD files
|
||||
'.github/workflows',
|
||||
'.gitlab-ci.yml',
|
||||
'.circleci/config.yml'
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -63,15 +63,15 @@ export const PROJECT_MARKERS = [
|
||||
* @returns {string} - Absolute path to the package installation directory
|
||||
*/
|
||||
export function getPackagePath() {
|
||||
// When running from source, __dirname is the directory containing this file
|
||||
// When running from npm, we need to find the package root
|
||||
const thisFilePath = fileURLToPath(import.meta.url);
|
||||
const thisFileDir = path.dirname(thisFilePath);
|
||||
|
||||
// Navigate from core/utils up to the package root
|
||||
// In dev: /path/to/task-master/mcp-server/src/core/utils -> /path/to/task-master
|
||||
// In npm: /path/to/node_modules/task-master/mcp-server/src/core/utils -> /path/to/node_modules/task-master
|
||||
return path.resolve(thisFileDir, '../../../../');
|
||||
// When running from source, __dirname is the directory containing this file
|
||||
// When running from npm, we need to find the package root
|
||||
const thisFilePath = fileURLToPath(import.meta.url);
|
||||
const thisFileDir = path.dirname(thisFilePath);
|
||||
|
||||
// Navigate from core/utils up to the package root
|
||||
// In dev: /path/to/task-master/mcp-server/src/core/utils -> /path/to/task-master
|
||||
// In npm: /path/to/node_modules/task-master/mcp-server/src/core/utils -> /path/to/node_modules/task-master
|
||||
return path.resolve(thisFileDir, '../../../../');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -82,62 +82,73 @@ export function getPackagePath() {
|
||||
* @throws {Error} - If tasks.json cannot be found.
|
||||
*/
|
||||
export function findTasksJsonPath(args, log) {
|
||||
// PRECEDENCE ORDER for finding tasks.json:
|
||||
// 1. Explicitly provided `projectRoot` in args (Highest priority, expected in MCP context)
|
||||
// 2. Previously found/cached `lastFoundProjectRoot` (primarily for CLI performance)
|
||||
// 3. Search upwards from current working directory (`process.cwd()`) - CLI usage
|
||||
|
||||
// 1. If project root is explicitly provided (e.g., from MCP session), use it directly
|
||||
if (args.projectRoot) {
|
||||
const projectRoot = args.projectRoot;
|
||||
log.info(`Using explicitly provided project root: ${projectRoot}`);
|
||||
try {
|
||||
// This will throw if tasks.json isn't found within this root
|
||||
return findTasksJsonInDirectory(projectRoot, args.file, log);
|
||||
} catch (error) {
|
||||
// Include debug info in error
|
||||
const debugInfo = {
|
||||
projectRoot,
|
||||
currentDir: process.cwd(),
|
||||
serverDir: path.dirname(process.argv[1]),
|
||||
possibleProjectRoot: path.resolve(path.dirname(process.argv[1]), '../..'),
|
||||
lastFoundProjectRoot,
|
||||
searchedPaths: error.message
|
||||
};
|
||||
|
||||
error.message = `Tasks file not found in any of the expected locations relative to project root "${projectRoot}" (from session).\nDebug Info: ${JSON.stringify(debugInfo, null, 2)}`;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Fallback logic primarily for CLI or when projectRoot isn't passed ---
|
||||
// PRECEDENCE ORDER for finding tasks.json:
|
||||
// 1. Explicitly provided `projectRoot` in args (Highest priority, expected in MCP context)
|
||||
// 2. Previously found/cached `lastFoundProjectRoot` (primarily for CLI performance)
|
||||
// 3. Search upwards from current working directory (`process.cwd()`) - CLI usage
|
||||
|
||||
// 2. If we have a last known project root that worked, try it first
|
||||
if (lastFoundProjectRoot) {
|
||||
log.info(`Trying last known project root: ${lastFoundProjectRoot}`);
|
||||
try {
|
||||
// Use the cached root
|
||||
const tasksPath = findTasksJsonInDirectory(lastFoundProjectRoot, args.file, log);
|
||||
return tasksPath; // Return if found in cached root
|
||||
} catch (error) {
|
||||
log.info(`Task file not found in last known project root, continuing search.`);
|
||||
// Continue with search if not found in cache
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Start search from current directory (most common CLI scenario)
|
||||
const startDir = process.cwd();
|
||||
log.info(`Searching for tasks.json starting from current directory: ${startDir}`);
|
||||
|
||||
// Try to find tasks.json by walking up the directory tree from cwd
|
||||
try {
|
||||
// This will throw if not found in the CWD tree
|
||||
return findTasksJsonWithParentSearch(startDir, args.file, log);
|
||||
} catch (error) {
|
||||
// If all attempts fail, augment and throw the original error from CWD search
|
||||
error.message = `${error.message}\n\nPossible solutions:\n1. Run the command from your project directory containing tasks.json\n2. Use --project-root=/path/to/project to specify the project location (if using CLI)\n3. Ensure the project root is correctly passed from the client (if using MCP)\n\nCurrent working directory: ${startDir}\nLast known project root: ${lastFoundProjectRoot}\nProject root from args: ${args.projectRoot}`;
|
||||
throw error;
|
||||
}
|
||||
// 1. If project root is explicitly provided (e.g., from MCP session), use it directly
|
||||
if (args.projectRoot) {
|
||||
const projectRoot = args.projectRoot;
|
||||
log.info(`Using explicitly provided project root: ${projectRoot}`);
|
||||
try {
|
||||
// This will throw if tasks.json isn't found within this root
|
||||
return findTasksJsonInDirectory(projectRoot, args.file, log);
|
||||
} catch (error) {
|
||||
// Include debug info in error
|
||||
const debugInfo = {
|
||||
projectRoot,
|
||||
currentDir: process.cwd(),
|
||||
serverDir: path.dirname(process.argv[1]),
|
||||
possibleProjectRoot: path.resolve(
|
||||
path.dirname(process.argv[1]),
|
||||
'../..'
|
||||
),
|
||||
lastFoundProjectRoot,
|
||||
searchedPaths: error.message
|
||||
};
|
||||
|
||||
error.message = `Tasks file not found in any of the expected locations relative to project root "${projectRoot}" (from session).\nDebug Info: ${JSON.stringify(debugInfo, null, 2)}`;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Fallback logic primarily for CLI or when projectRoot isn't passed ---
|
||||
|
||||
// 2. If we have a last known project root that worked, try it first
|
||||
if (lastFoundProjectRoot) {
|
||||
log.info(`Trying last known project root: ${lastFoundProjectRoot}`);
|
||||
try {
|
||||
// Use the cached root
|
||||
const tasksPath = findTasksJsonInDirectory(
|
||||
lastFoundProjectRoot,
|
||||
args.file,
|
||||
log
|
||||
);
|
||||
return tasksPath; // Return if found in cached root
|
||||
} catch (error) {
|
||||
log.info(
|
||||
`Task file not found in last known project root, continuing search.`
|
||||
);
|
||||
// Continue with search if not found in cache
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Start search from current directory (most common CLI scenario)
|
||||
const startDir = process.cwd();
|
||||
log.info(
|
||||
`Searching for tasks.json starting from current directory: ${startDir}`
|
||||
);
|
||||
|
||||
// Try to find tasks.json by walking up the directory tree from cwd
|
||||
try {
|
||||
// This will throw if not found in the CWD tree
|
||||
return findTasksJsonWithParentSearch(startDir, args.file, log);
|
||||
} catch (error) {
|
||||
// If all attempts fail, augment and throw the original error from CWD search
|
||||
error.message = `${error.message}\n\nPossible solutions:\n1. Run the command from your project directory containing tasks.json\n2. Use --project-root=/path/to/project to specify the project location (if using CLI)\n3. Ensure the project root is correctly passed from the client (if using MCP)\n\nCurrent working directory: ${startDir}\nLast known project root: ${lastFoundProjectRoot}\nProject root from args: ${args.projectRoot}`;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -146,11 +157,11 @@ export function findTasksJsonPath(args, log) {
|
||||
* @returns {boolean} - True if the directory contains any project markers
|
||||
*/
|
||||
function hasProjectMarkers(dirPath) {
|
||||
return PROJECT_MARKERS.some(marker => {
|
||||
const markerPath = path.join(dirPath, marker);
|
||||
// Check if the marker exists as either a file or directory
|
||||
return fs.existsSync(markerPath);
|
||||
});
|
||||
return PROJECT_MARKERS.some((marker) => {
|
||||
const markerPath = path.join(dirPath, marker);
|
||||
// Check if the marker exists as either a file or directory
|
||||
return fs.existsSync(markerPath);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -162,39 +173,41 @@ function hasProjectMarkers(dirPath) {
|
||||
* @throws {Error} - If tasks.json cannot be found
|
||||
*/
|
||||
function findTasksJsonInDirectory(dirPath, explicitFilePath, log) {
|
||||
const possiblePaths = [];
|
||||
const possiblePaths = [];
|
||||
|
||||
// 1. If a file is explicitly provided relative to dirPath
|
||||
if (explicitFilePath) {
|
||||
possiblePaths.push(path.resolve(dirPath, explicitFilePath));
|
||||
}
|
||||
// 1. If a file is explicitly provided relative to dirPath
|
||||
if (explicitFilePath) {
|
||||
possiblePaths.push(path.resolve(dirPath, explicitFilePath));
|
||||
}
|
||||
|
||||
// 2. Check the standard locations relative to dirPath
|
||||
possiblePaths.push(
|
||||
path.join(dirPath, 'tasks.json'),
|
||||
path.join(dirPath, 'tasks', 'tasks.json')
|
||||
);
|
||||
// 2. Check the standard locations relative to dirPath
|
||||
possiblePaths.push(
|
||||
path.join(dirPath, 'tasks.json'),
|
||||
path.join(dirPath, 'tasks', 'tasks.json')
|
||||
);
|
||||
|
||||
log.info(`Checking potential task file paths: ${possiblePaths.join(', ')}`);
|
||||
log.info(`Checking potential task file paths: ${possiblePaths.join(', ')}`);
|
||||
|
||||
// Find the first existing path
|
||||
for (const p of possiblePaths) {
|
||||
log.info(`Checking if exists: ${p}`);
|
||||
const exists = fs.existsSync(p);
|
||||
log.info(`Path ${p} exists: ${exists}`);
|
||||
|
||||
if (exists) {
|
||||
log.info(`Found tasks file at: ${p}`);
|
||||
// Store the project root for future use
|
||||
lastFoundProjectRoot = dirPath;
|
||||
return p;
|
||||
}
|
||||
}
|
||||
// Find the first existing path
|
||||
for (const p of possiblePaths) {
|
||||
log.info(`Checking if exists: ${p}`);
|
||||
const exists = fs.existsSync(p);
|
||||
log.info(`Path ${p} exists: ${exists}`);
|
||||
|
||||
// If no file was found, throw an error
|
||||
const error = new Error(`Tasks file not found in any of the expected locations relative to ${dirPath}: ${possiblePaths.join(', ')}`);
|
||||
error.code = 'TASKS_FILE_NOT_FOUND';
|
||||
throw error;
|
||||
if (exists) {
|
||||
log.info(`Found tasks file at: ${p}`);
|
||||
// Store the project root for future use
|
||||
lastFoundProjectRoot = dirPath;
|
||||
return p;
|
||||
}
|
||||
}
|
||||
|
||||
// If no file was found, throw an error
|
||||
const error = new Error(
|
||||
`Tasks file not found in any of the expected locations relative to ${dirPath}: ${possiblePaths.join(', ')}`
|
||||
);
|
||||
error.code = 'TASKS_FILE_NOT_FOUND';
|
||||
throw error;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -207,66 +220,74 @@ function findTasksJsonInDirectory(dirPath, explicitFilePath, log) {
|
||||
* @throws {Error} - If tasks.json cannot be found in any parent directory
|
||||
*/
|
||||
function findTasksJsonWithParentSearch(startDir, explicitFilePath, log) {
|
||||
let currentDir = startDir;
|
||||
const rootDir = path.parse(currentDir).root;
|
||||
|
||||
// Keep traversing up until we hit the root directory
|
||||
while (currentDir !== rootDir) {
|
||||
// First check for tasks.json directly
|
||||
try {
|
||||
return findTasksJsonInDirectory(currentDir, explicitFilePath, log);
|
||||
} catch (error) {
|
||||
// If tasks.json not found but the directory has project markers,
|
||||
// log it as a potential project root (helpful for debugging)
|
||||
if (hasProjectMarkers(currentDir)) {
|
||||
log.info(`Found project markers in ${currentDir}, but no tasks.json`);
|
||||
}
|
||||
|
||||
// Move up to parent directory
|
||||
const parentDir = path.dirname(currentDir);
|
||||
|
||||
// Check if we've reached the root
|
||||
if (parentDir === currentDir) {
|
||||
break;
|
||||
}
|
||||
|
||||
log.info(`Tasks file not found in ${currentDir}, searching in parent directory: ${parentDir}`);
|
||||
currentDir = parentDir;
|
||||
}
|
||||
}
|
||||
|
||||
// If we've searched all the way to the root and found nothing
|
||||
const error = new Error(`Tasks file not found in ${startDir} or any parent directory.`);
|
||||
error.code = 'TASKS_FILE_NOT_FOUND';
|
||||
throw error;
|
||||
let currentDir = startDir;
|
||||
const rootDir = path.parse(currentDir).root;
|
||||
|
||||
// Keep traversing up until we hit the root directory
|
||||
while (currentDir !== rootDir) {
|
||||
// First check for tasks.json directly
|
||||
try {
|
||||
return findTasksJsonInDirectory(currentDir, explicitFilePath, log);
|
||||
} catch (error) {
|
||||
// If tasks.json not found but the directory has project markers,
|
||||
// log it as a potential project root (helpful for debugging)
|
||||
if (hasProjectMarkers(currentDir)) {
|
||||
log.info(`Found project markers in ${currentDir}, but no tasks.json`);
|
||||
}
|
||||
|
||||
// Move up to parent directory
|
||||
const parentDir = path.dirname(currentDir);
|
||||
|
||||
// Check if we've reached the root
|
||||
if (parentDir === currentDir) {
|
||||
break;
|
||||
}
|
||||
|
||||
log.info(
|
||||
`Tasks file not found in ${currentDir}, searching in parent directory: ${parentDir}`
|
||||
);
|
||||
currentDir = parentDir;
|
||||
}
|
||||
}
|
||||
|
||||
// If we've searched all the way to the root and found nothing
|
||||
const error = new Error(
|
||||
`Tasks file not found in ${startDir} or any parent directory.`
|
||||
);
|
||||
error.code = 'TASKS_FILE_NOT_FOUND';
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Note: findTasksWithNpmConsideration is not used by findTasksJsonPath and might be legacy or used elsewhere.
|
||||
// If confirmed unused, it could potentially be removed in a separate cleanup.
|
||||
function findTasksWithNpmConsideration(startDir, log) {
|
||||
// First try our recursive parent search from cwd
|
||||
try {
|
||||
return findTasksJsonWithParentSearch(startDir, null, log);
|
||||
} catch (error) {
|
||||
// If that fails, try looking relative to the executable location
|
||||
const execPath = process.argv[1];
|
||||
const execDir = path.dirname(execPath);
|
||||
log.info(`Looking for tasks file relative to executable at: ${execDir}`);
|
||||
|
||||
try {
|
||||
return findTasksJsonWithParentSearch(execDir, null, log);
|
||||
} catch (secondError) {
|
||||
// If that also fails, check standard locations in user's home directory
|
||||
const homeDir = os.homedir();
|
||||
log.info(`Looking for tasks file in home directory: ${homeDir}`);
|
||||
|
||||
try {
|
||||
// Check standard locations in home dir
|
||||
return findTasksJsonInDirectory(path.join(homeDir, '.task-master'), null, log);
|
||||
} catch (thirdError) {
|
||||
// If all approaches fail, throw the original error
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// First try our recursive parent search from cwd
|
||||
try {
|
||||
return findTasksJsonWithParentSearch(startDir, null, log);
|
||||
} catch (error) {
|
||||
// If that fails, try looking relative to the executable location
|
||||
const execPath = process.argv[1];
|
||||
const execDir = path.dirname(execPath);
|
||||
log.info(`Looking for tasks file relative to executable at: ${execDir}`);
|
||||
|
||||
try {
|
||||
return findTasksJsonWithParentSearch(execDir, null, log);
|
||||
} catch (secondError) {
|
||||
// If that also fails, check standard locations in user's home directory
|
||||
const homeDir = os.homedir();
|
||||
log.info(`Looking for tasks file in home directory: ${homeDir}`);
|
||||
|
||||
try {
|
||||
// Check standard locations in home dir
|
||||
return findTasksJsonInDirectory(
|
||||
path.join(homeDir, '.task-master'),
|
||||
null,
|
||||
log
|
||||
);
|
||||
} catch (thirdError) {
|
||||
// If all approaches fail, throw the original error
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user