chore: passes tests and linting
This commit is contained in:
@@ -24,9 +24,9 @@ import {
|
|||||||
getAzureBaseURL,
|
getAzureBaseURL,
|
||||||
getBedrockBaseURL,
|
getBedrockBaseURL,
|
||||||
getVertexProjectId,
|
getVertexProjectId,
|
||||||
getVertexLocation,
|
getVertexLocation
|
||||||
} from "./config-manager.js";
|
} from './config-manager.js';
|
||||||
import { log, findProjectRoot, resolveEnvVariable } from "./utils.js";
|
import { log, findProjectRoot, resolveEnvVariable } from './utils.js';
|
||||||
|
|
||||||
// Import provider classes
|
// Import provider classes
|
||||||
import {
|
import {
|
||||||
@@ -39,8 +39,8 @@ import {
|
|||||||
OllamaAIProvider,
|
OllamaAIProvider,
|
||||||
BedrockAIProvider,
|
BedrockAIProvider,
|
||||||
AzureProvider,
|
AzureProvider,
|
||||||
VertexAIProvider,
|
VertexAIProvider
|
||||||
} from "../../src/ai-providers/index.js";
|
} from '../../src/ai-providers/index.js';
|
||||||
|
|
||||||
// Create provider instances
|
// Create provider instances
|
||||||
const PROVIDERS = {
|
const PROVIDERS = {
|
||||||
@@ -53,36 +53,36 @@ const PROVIDERS = {
|
|||||||
ollama: new OllamaAIProvider(),
|
ollama: new OllamaAIProvider(),
|
||||||
bedrock: new BedrockAIProvider(),
|
bedrock: new BedrockAIProvider(),
|
||||||
azure: new AzureProvider(),
|
azure: new AzureProvider(),
|
||||||
vertex: new VertexAIProvider(),
|
vertex: new VertexAIProvider()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper function to get cost for a specific model
|
// Helper function to get cost for a specific model
|
||||||
function _getCostForModel(providerName, modelId) {
|
function _getCostForModel(providerName, modelId) {
|
||||||
if (!MODEL_MAP || !MODEL_MAP[providerName]) {
|
if (!MODEL_MAP || !MODEL_MAP[providerName]) {
|
||||||
log(
|
log(
|
||||||
"warn",
|
'warn',
|
||||||
`Provider "${providerName}" not found in MODEL_MAP. Cannot determine cost for model ${modelId}.`
|
`Provider "${providerName}" not found in MODEL_MAP. Cannot determine cost for model ${modelId}.`
|
||||||
);
|
);
|
||||||
return { inputCost: 0, outputCost: 0, currency: "USD" }; // Default to zero cost
|
return { inputCost: 0, outputCost: 0, currency: 'USD' }; // Default to zero cost
|
||||||
}
|
}
|
||||||
|
|
||||||
const modelData = MODEL_MAP[providerName].find((m) => m.id === modelId);
|
const modelData = MODEL_MAP[providerName].find((m) => m.id === modelId);
|
||||||
|
|
||||||
if (!modelData || !modelData.cost_per_1m_tokens) {
|
if (!modelData || !modelData.cost_per_1m_tokens) {
|
||||||
log(
|
log(
|
||||||
"debug",
|
'debug',
|
||||||
`Cost data not found for model "${modelId}" under provider "${providerName}". Assuming zero cost.`
|
`Cost data not found for model "${modelId}" under provider "${providerName}". Assuming zero cost.`
|
||||||
);
|
);
|
||||||
return { inputCost: 0, outputCost: 0, currency: "USD" }; // Default to zero cost
|
return { inputCost: 0, outputCost: 0, currency: 'USD' }; // Default to zero cost
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure currency is part of the returned object, defaulting if not present
|
// Ensure currency is part of the returned object, defaulting if not present
|
||||||
const currency = modelData.cost_per_1m_tokens.currency || "USD";
|
const currency = modelData.cost_per_1m_tokens.currency || 'USD';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
inputCost: modelData.cost_per_1m_tokens.input || 0,
|
inputCost: modelData.cost_per_1m_tokens.input || 0,
|
||||||
outputCost: modelData.cost_per_1m_tokens.output || 0,
|
outputCost: modelData.cost_per_1m_tokens.output || 0,
|
||||||
currency: currency,
|
currency: currency
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,13 +92,13 @@ const INITIAL_RETRY_DELAY_MS = 1000;
|
|||||||
|
|
||||||
// Helper function to check if an error is retryable
|
// Helper function to check if an error is retryable
|
||||||
function isRetryableError(error) {
|
function isRetryableError(error) {
|
||||||
const errorMessage = error.message?.toLowerCase() || "";
|
const errorMessage = error.message?.toLowerCase() || '';
|
||||||
return (
|
return (
|
||||||
errorMessage.includes("rate limit") ||
|
errorMessage.includes('rate limit') ||
|
||||||
errorMessage.includes("overloaded") ||
|
errorMessage.includes('overloaded') ||
|
||||||
errorMessage.includes("service temporarily unavailable") ||
|
errorMessage.includes('service temporarily unavailable') ||
|
||||||
errorMessage.includes("timeout") ||
|
errorMessage.includes('timeout') ||
|
||||||
errorMessage.includes("network error") ||
|
errorMessage.includes('network error') ||
|
||||||
error.status === 429 ||
|
error.status === 429 ||
|
||||||
error.status >= 500
|
error.status >= 500
|
||||||
);
|
);
|
||||||
@@ -123,7 +123,7 @@ function _extractErrorMessage(error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Attempt 3: Look for nested error message in response body if it's JSON string
|
// Attempt 3: Look for nested error message in response body if it's JSON string
|
||||||
if (typeof error?.responseBody === "string") {
|
if (typeof error?.responseBody === 'string') {
|
||||||
try {
|
try {
|
||||||
const body = JSON.parse(error.responseBody);
|
const body = JSON.parse(error.responseBody);
|
||||||
if (body?.error?.message) {
|
if (body?.error?.message) {
|
||||||
@@ -135,20 +135,20 @@ function _extractErrorMessage(error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Attempt 4: Use the top-level message if it exists
|
// Attempt 4: Use the top-level message if it exists
|
||||||
if (typeof error?.message === "string" && error.message) {
|
if (typeof error?.message === 'string' && error.message) {
|
||||||
return error.message;
|
return error.message;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempt 5: Handle simple string errors
|
// Attempt 5: Handle simple string errors
|
||||||
if (typeof error === "string") {
|
if (typeof error === 'string') {
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback
|
// Fallback
|
||||||
return "An unknown AI service error occurred.";
|
return 'An unknown AI service error occurred.';
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Safety net
|
// Safety net
|
||||||
return "Failed to extract error message.";
|
return 'Failed to extract error message.';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,17 +162,17 @@ function _extractErrorMessage(error) {
|
|||||||
*/
|
*/
|
||||||
function _resolveApiKey(providerName, session, projectRoot = null) {
|
function _resolveApiKey(providerName, session, projectRoot = null) {
|
||||||
const keyMap = {
|
const keyMap = {
|
||||||
openai: "OPENAI_API_KEY",
|
openai: 'OPENAI_API_KEY',
|
||||||
anthropic: "ANTHROPIC_API_KEY",
|
anthropic: 'ANTHROPIC_API_KEY',
|
||||||
google: "GOOGLE_API_KEY",
|
google: 'GOOGLE_API_KEY',
|
||||||
perplexity: "PERPLEXITY_API_KEY",
|
perplexity: 'PERPLEXITY_API_KEY',
|
||||||
mistral: "MISTRAL_API_KEY",
|
mistral: 'MISTRAL_API_KEY',
|
||||||
azure: "AZURE_OPENAI_API_KEY",
|
azure: 'AZURE_OPENAI_API_KEY',
|
||||||
openrouter: "OPENROUTER_API_KEY",
|
openrouter: 'OPENROUTER_API_KEY',
|
||||||
xai: "XAI_API_KEY",
|
xai: 'XAI_API_KEY',
|
||||||
ollama: "OLLAMA_API_KEY",
|
ollama: 'OLLAMA_API_KEY',
|
||||||
bedrock: "AWS_ACCESS_KEY_ID",
|
bedrock: 'AWS_ACCESS_KEY_ID',
|
||||||
vertex: "GOOGLE_API_KEY",
|
vertex: 'GOOGLE_API_KEY'
|
||||||
};
|
};
|
||||||
|
|
||||||
const envVarName = keyMap[providerName];
|
const envVarName = keyMap[providerName];
|
||||||
@@ -185,7 +185,7 @@ function _resolveApiKey(providerName, session, projectRoot = null) {
|
|||||||
const apiKey = resolveEnvVariable(envVarName, session, projectRoot);
|
const apiKey = resolveEnvVariable(envVarName, session, projectRoot);
|
||||||
|
|
||||||
// Special handling for providers that can use alternative auth
|
// Special handling for providers that can use alternative auth
|
||||||
if (providerName === "ollama" || providerName === "bedrock") {
|
if (providerName === 'ollama' || providerName === 'bedrock') {
|
||||||
return apiKey || null;
|
return apiKey || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,7 +223,7 @@ async function _attemptProviderCallWithRetries(
|
|||||||
try {
|
try {
|
||||||
if (getDebugFlag()) {
|
if (getDebugFlag()) {
|
||||||
log(
|
log(
|
||||||
"info",
|
'info',
|
||||||
`Attempt ${retries + 1}/${MAX_RETRIES + 1} calling ${fnName} (Provider: ${providerName}, Model: ${modelId}, Role: ${attemptRole})`
|
`Attempt ${retries + 1}/${MAX_RETRIES + 1} calling ${fnName} (Provider: ${providerName}, Model: ${modelId}, Role: ${attemptRole})`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -233,14 +233,14 @@ async function _attemptProviderCallWithRetries(
|
|||||||
|
|
||||||
if (getDebugFlag()) {
|
if (getDebugFlag()) {
|
||||||
log(
|
log(
|
||||||
"info",
|
'info',
|
||||||
`${fnName} succeeded for role ${attemptRole} (Provider: ${providerName}) on attempt ${retries + 1}`
|
`${fnName} succeeded for role ${attemptRole} (Provider: ${providerName}) on attempt ${retries + 1}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log(
|
log(
|
||||||
"warn",
|
'warn',
|
||||||
`Attempt ${retries + 1} failed for role ${attemptRole} (${fnName} / ${providerName}): ${error.message}`
|
`Attempt ${retries + 1} failed for role ${attemptRole} (${fnName} / ${providerName}): ${error.message}`
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -248,13 +248,13 @@ async function _attemptProviderCallWithRetries(
|
|||||||
retries++;
|
retries++;
|
||||||
const delay = INITIAL_RETRY_DELAY_MS * Math.pow(2, retries - 1);
|
const delay = INITIAL_RETRY_DELAY_MS * Math.pow(2, retries - 1);
|
||||||
log(
|
log(
|
||||||
"info",
|
'info',
|
||||||
`Something went wrong on the provider side. Retrying in ${delay / 1000}s...`
|
`Something went wrong on the provider side. Retrying in ${delay / 1000}s...`
|
||||||
);
|
);
|
||||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||||
} else {
|
} else {
|
||||||
log(
|
log(
|
||||||
"error",
|
'error',
|
||||||
`Something went wrong on the provider side. Max retries reached for role ${attemptRole} (${fnName} / ${providerName}).`
|
`Something went wrong on the provider side. Max retries reached for role ${attemptRole} (${fnName} / ${providerName}).`
|
||||||
);
|
);
|
||||||
throw error;
|
throw error;
|
||||||
@@ -296,11 +296,11 @@ async function _unifiedServiceRunner(serviceType, params) {
|
|||||||
...restApiParams
|
...restApiParams
|
||||||
} = params;
|
} = params;
|
||||||
if (getDebugFlag()) {
|
if (getDebugFlag()) {
|
||||||
log("info", `${serviceType}Service called`, {
|
log('info', `${serviceType}Service called`, {
|
||||||
role: initialRole,
|
role: initialRole,
|
||||||
commandName,
|
commandName,
|
||||||
outputType,
|
outputType,
|
||||||
projectRoot,
|
projectRoot
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,23 +308,23 @@ async function _unifiedServiceRunner(serviceType, params) {
|
|||||||
const userId = getUserId(effectiveProjectRoot);
|
const userId = getUserId(effectiveProjectRoot);
|
||||||
|
|
||||||
let sequence;
|
let sequence;
|
||||||
if (initialRole === "main") {
|
if (initialRole === 'main') {
|
||||||
sequence = ["main", "fallback", "research"];
|
sequence = ['main', 'fallback', 'research'];
|
||||||
} else if (initialRole === "research") {
|
} else if (initialRole === 'research') {
|
||||||
sequence = ["research", "fallback", "main"];
|
sequence = ['research', 'fallback', 'main'];
|
||||||
} else if (initialRole === "fallback") {
|
} else if (initialRole === 'fallback') {
|
||||||
sequence = ["fallback", "main", "research"];
|
sequence = ['fallback', 'main', 'research'];
|
||||||
} else {
|
} else {
|
||||||
log(
|
log(
|
||||||
"warn",
|
'warn',
|
||||||
`Unknown initial role: ${initialRole}. Defaulting to main -> fallback -> research sequence.`
|
`Unknown initial role: ${initialRole}. Defaulting to main -> fallback -> research sequence.`
|
||||||
);
|
);
|
||||||
sequence = ["main", "fallback", "research"];
|
sequence = ['main', 'fallback', 'research'];
|
||||||
}
|
}
|
||||||
|
|
||||||
let lastError = null;
|
let lastError = null;
|
||||||
let lastCleanErrorMessage =
|
let lastCleanErrorMessage =
|
||||||
"AI service call failed for all configured roles.";
|
'AI service call failed for all configured roles.';
|
||||||
|
|
||||||
for (const currentRole of sequence) {
|
for (const currentRole of sequence) {
|
||||||
let providerName,
|
let providerName,
|
||||||
@@ -337,20 +337,20 @@ async function _unifiedServiceRunner(serviceType, params) {
|
|||||||
telemetryData = null;
|
telemetryData = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
log("info", `New AI service call with role: ${currentRole}`);
|
log('info', `New AI service call with role: ${currentRole}`);
|
||||||
|
|
||||||
if (currentRole === "main") {
|
if (currentRole === 'main') {
|
||||||
providerName = getMainProvider(effectiveProjectRoot);
|
providerName = getMainProvider(effectiveProjectRoot);
|
||||||
modelId = getMainModelId(effectiveProjectRoot);
|
modelId = getMainModelId(effectiveProjectRoot);
|
||||||
} else if (currentRole === "research") {
|
} else if (currentRole === 'research') {
|
||||||
providerName = getResearchProvider(effectiveProjectRoot);
|
providerName = getResearchProvider(effectiveProjectRoot);
|
||||||
modelId = getResearchModelId(effectiveProjectRoot);
|
modelId = getResearchModelId(effectiveProjectRoot);
|
||||||
} else if (currentRole === "fallback") {
|
} else if (currentRole === 'fallback') {
|
||||||
providerName = getFallbackProvider(effectiveProjectRoot);
|
providerName = getFallbackProvider(effectiveProjectRoot);
|
||||||
modelId = getFallbackModelId(effectiveProjectRoot);
|
modelId = getFallbackModelId(effectiveProjectRoot);
|
||||||
} else {
|
} else {
|
||||||
log(
|
log(
|
||||||
"error",
|
'error',
|
||||||
`Unknown role encountered in _unifiedServiceRunner: ${currentRole}`
|
`Unknown role encountered in _unifiedServiceRunner: ${currentRole}`
|
||||||
);
|
);
|
||||||
lastError =
|
lastError =
|
||||||
@@ -360,7 +360,7 @@ async function _unifiedServiceRunner(serviceType, params) {
|
|||||||
|
|
||||||
if (!providerName || !modelId) {
|
if (!providerName || !modelId) {
|
||||||
log(
|
log(
|
||||||
"warn",
|
'warn',
|
||||||
`Skipping role '${currentRole}': Provider or Model ID not configured.`
|
`Skipping role '${currentRole}': Provider or Model ID not configured.`
|
||||||
);
|
);
|
||||||
lastError =
|
lastError =
|
||||||
@@ -375,7 +375,7 @@ async function _unifiedServiceRunner(serviceType, params) {
|
|||||||
provider = PROVIDERS[providerName?.toLowerCase()];
|
provider = PROVIDERS[providerName?.toLowerCase()];
|
||||||
if (!provider) {
|
if (!provider) {
|
||||||
log(
|
log(
|
||||||
"warn",
|
'warn',
|
||||||
`Skipping role '${currentRole}': Provider '${providerName}' not supported.`
|
`Skipping role '${currentRole}': Provider '${providerName}' not supported.`
|
||||||
);
|
);
|
||||||
lastError =
|
lastError =
|
||||||
@@ -385,10 +385,10 @@ async function _unifiedServiceRunner(serviceType, params) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check API key if needed
|
// Check API key if needed
|
||||||
if (providerName?.toLowerCase() !== "ollama") {
|
if (providerName?.toLowerCase() !== 'ollama') {
|
||||||
if (!isApiKeySet(providerName, session, effectiveProjectRoot)) {
|
if (!isApiKeySet(providerName, session, effectiveProjectRoot)) {
|
||||||
log(
|
log(
|
||||||
"warn",
|
'warn',
|
||||||
`Skipping role '${currentRole}' (Provider: ${providerName}): API key not set or invalid.`
|
`Skipping role '${currentRole}' (Provider: ${providerName}): API key not set or invalid.`
|
||||||
);
|
);
|
||||||
lastError =
|
lastError =
|
||||||
@@ -404,17 +404,17 @@ async function _unifiedServiceRunner(serviceType, params) {
|
|||||||
baseURL = getBaseUrlForRole(currentRole, effectiveProjectRoot);
|
baseURL = getBaseUrlForRole(currentRole, effectiveProjectRoot);
|
||||||
|
|
||||||
// For Azure, use the global Azure base URL if role-specific URL is not configured
|
// For Azure, use the global Azure base URL if role-specific URL is not configured
|
||||||
if (providerName?.toLowerCase() === "azure" && !baseURL) {
|
if (providerName?.toLowerCase() === 'azure' && !baseURL) {
|
||||||
baseURL = getAzureBaseURL(effectiveProjectRoot);
|
baseURL = getAzureBaseURL(effectiveProjectRoot);
|
||||||
log("debug", `Using global Azure base URL: ${baseURL}`);
|
log('debug', `Using global Azure base URL: ${baseURL}`);
|
||||||
} else if (providerName?.toLowerCase() === "ollama" && !baseURL) {
|
} else if (providerName?.toLowerCase() === 'ollama' && !baseURL) {
|
||||||
// For Ollama, use the global Ollama base URL if role-specific URL is not configured
|
// For Ollama, use the global Ollama base URL if role-specific URL is not configured
|
||||||
baseURL = getOllamaBaseURL(effectiveProjectRoot);
|
baseURL = getOllamaBaseURL(effectiveProjectRoot);
|
||||||
log("debug", `Using global Ollama base URL: ${baseURL}`);
|
log('debug', `Using global Ollama base URL: ${baseURL}`);
|
||||||
} else if (providerName?.toLowerCase() === "bedrock" && !baseURL) {
|
} else if (providerName?.toLowerCase() === 'bedrock' && !baseURL) {
|
||||||
// For Bedrock, use the global Bedrock base URL if role-specific URL is not configured
|
// For Bedrock, use the global Bedrock base URL if role-specific URL is not configured
|
||||||
baseURL = getBedrockBaseURL(effectiveProjectRoot);
|
baseURL = getBedrockBaseURL(effectiveProjectRoot);
|
||||||
log("debug", `Using global Bedrock base URL: ${baseURL}`);
|
log('debug', `Using global Bedrock base URL: ${baseURL}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get AI parameters for the current role
|
// Get AI parameters for the current role
|
||||||
@@ -429,12 +429,12 @@ async function _unifiedServiceRunner(serviceType, params) {
|
|||||||
let providerSpecificParams = {};
|
let providerSpecificParams = {};
|
||||||
|
|
||||||
// Handle Vertex AI specific configuration
|
// Handle Vertex AI specific configuration
|
||||||
if (providerName?.toLowerCase() === "vertex") {
|
if (providerName?.toLowerCase() === 'vertex') {
|
||||||
// Get Vertex project ID and location
|
// Get Vertex project ID and location
|
||||||
const projectId =
|
const projectId =
|
||||||
getVertexProjectId(effectiveProjectRoot) ||
|
getVertexProjectId(effectiveProjectRoot) ||
|
||||||
resolveEnvVariable(
|
resolveEnvVariable(
|
||||||
"VERTEX_PROJECT_ID",
|
'VERTEX_PROJECT_ID',
|
||||||
session,
|
session,
|
||||||
effectiveProjectRoot
|
effectiveProjectRoot
|
||||||
);
|
);
|
||||||
@@ -442,15 +442,15 @@ async function _unifiedServiceRunner(serviceType, params) {
|
|||||||
const location =
|
const location =
|
||||||
getVertexLocation(effectiveProjectRoot) ||
|
getVertexLocation(effectiveProjectRoot) ||
|
||||||
resolveEnvVariable(
|
resolveEnvVariable(
|
||||||
"VERTEX_LOCATION",
|
'VERTEX_LOCATION',
|
||||||
session,
|
session,
|
||||||
effectiveProjectRoot
|
effectiveProjectRoot
|
||||||
) ||
|
) ||
|
||||||
"us-central1";
|
'us-central1';
|
||||||
|
|
||||||
// Get credentials path if available
|
// Get credentials path if available
|
||||||
const credentialsPath = resolveEnvVariable(
|
const credentialsPath = resolveEnvVariable(
|
||||||
"GOOGLE_APPLICATION_CREDENTIALS",
|
'GOOGLE_APPLICATION_CREDENTIALS',
|
||||||
session,
|
session,
|
||||||
effectiveProjectRoot
|
effectiveProjectRoot
|
||||||
);
|
);
|
||||||
@@ -459,18 +459,18 @@ async function _unifiedServiceRunner(serviceType, params) {
|
|||||||
providerSpecificParams = {
|
providerSpecificParams = {
|
||||||
projectId,
|
projectId,
|
||||||
location,
|
location,
|
||||||
...(credentialsPath && { credentials: { credentialsFromEnv: true } }),
|
...(credentialsPath && { credentials: { credentialsFromEnv: true } })
|
||||||
};
|
};
|
||||||
|
|
||||||
log(
|
log(
|
||||||
"debug",
|
'debug',
|
||||||
`Using Vertex AI configuration: Project ID=${projectId}, Location=${location}`
|
`Using Vertex AI configuration: Project ID=${projectId}, Location=${location}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const messages = [];
|
const messages = [];
|
||||||
if (systemPrompt) {
|
if (systemPrompt) {
|
||||||
messages.push({ role: "system", content: systemPrompt });
|
messages.push({ role: 'system', content: systemPrompt });
|
||||||
}
|
}
|
||||||
|
|
||||||
// IN THE FUTURE WHEN DOING CONTEXT IMPROVEMENTS
|
// IN THE FUTURE WHEN DOING CONTEXT IMPROVEMENTS
|
||||||
@@ -492,9 +492,9 @@ async function _unifiedServiceRunner(serviceType, params) {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
if (prompt) {
|
if (prompt) {
|
||||||
messages.push({ role: "user", content: prompt });
|
messages.push({ role: 'user', content: prompt });
|
||||||
} else {
|
} else {
|
||||||
throw new Error("User prompt content is missing.");
|
throw new Error('User prompt content is missing.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const callParams = {
|
const callParams = {
|
||||||
@@ -504,9 +504,9 @@ async function _unifiedServiceRunner(serviceType, params) {
|
|||||||
temperature: roleParams.temperature,
|
temperature: roleParams.temperature,
|
||||||
messages,
|
messages,
|
||||||
...(baseURL && { baseURL }),
|
...(baseURL && { baseURL }),
|
||||||
...(serviceType === "generateObject" && { schema, objectName }),
|
...(serviceType === 'generateObject' && { schema, objectName }),
|
||||||
...providerSpecificParams,
|
...providerSpecificParams,
|
||||||
...restApiParams,
|
...restApiParams
|
||||||
};
|
};
|
||||||
|
|
||||||
providerResponse = await _attemptProviderCallWithRetries(
|
providerResponse = await _attemptProviderCallWithRetries(
|
||||||
@@ -527,7 +527,7 @@ async function _unifiedServiceRunner(serviceType, params) {
|
|||||||
modelId,
|
modelId,
|
||||||
inputTokens: providerResponse.usage.inputTokens,
|
inputTokens: providerResponse.usage.inputTokens,
|
||||||
outputTokens: providerResponse.usage.outputTokens,
|
outputTokens: providerResponse.usage.outputTokens,
|
||||||
outputType,
|
outputType
|
||||||
});
|
});
|
||||||
} catch (telemetryError) {
|
} catch (telemetryError) {
|
||||||
// logAiUsage already logs its own errors and returns null on failure
|
// logAiUsage already logs its own errors and returns null on failure
|
||||||
@@ -535,21 +535,21 @@ async function _unifiedServiceRunner(serviceType, params) {
|
|||||||
}
|
}
|
||||||
} else if (userId && providerResponse && !providerResponse.usage) {
|
} else if (userId && providerResponse && !providerResponse.usage) {
|
||||||
log(
|
log(
|
||||||
"warn",
|
'warn',
|
||||||
`Cannot log telemetry for ${commandName} (${providerName}/${modelId}): AI result missing 'usage' data. (May be expected for streams)`
|
`Cannot log telemetry for ${commandName} (${providerName}/${modelId}): AI result missing 'usage' data. (May be expected for streams)`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let finalMainResult;
|
let finalMainResult;
|
||||||
if (serviceType === "generateText") {
|
if (serviceType === 'generateText') {
|
||||||
finalMainResult = providerResponse.text;
|
finalMainResult = providerResponse.text;
|
||||||
} else if (serviceType === "generateObject") {
|
} else if (serviceType === 'generateObject') {
|
||||||
finalMainResult = providerResponse.object;
|
finalMainResult = providerResponse.object;
|
||||||
} else if (serviceType === "streamText") {
|
} else if (serviceType === 'streamText') {
|
||||||
finalMainResult = providerResponse;
|
finalMainResult = providerResponse;
|
||||||
} else {
|
} else {
|
||||||
log(
|
log(
|
||||||
"error",
|
'error',
|
||||||
`Unknown serviceType in _unifiedServiceRunner: ${serviceType}`
|
`Unknown serviceType in _unifiedServiceRunner: ${serviceType}`
|
||||||
);
|
);
|
||||||
finalMainResult = providerResponse;
|
finalMainResult = providerResponse;
|
||||||
@@ -557,38 +557,38 @@ async function _unifiedServiceRunner(serviceType, params) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
mainResult: finalMainResult,
|
mainResult: finalMainResult,
|
||||||
telemetryData: telemetryData,
|
telemetryData: telemetryData
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const cleanMessage = _extractErrorMessage(error);
|
const cleanMessage = _extractErrorMessage(error);
|
||||||
log(
|
log(
|
||||||
"error",
|
'error',
|
||||||
`Service call failed for role ${currentRole} (Provider: ${providerName || "unknown"}, Model: ${modelId || "unknown"}): ${cleanMessage}`
|
`Service call failed for role ${currentRole} (Provider: ${providerName || 'unknown'}, Model: ${modelId || 'unknown'}): ${cleanMessage}`
|
||||||
);
|
);
|
||||||
lastError = error;
|
lastError = error;
|
||||||
lastCleanErrorMessage = cleanMessage;
|
lastCleanErrorMessage = cleanMessage;
|
||||||
|
|
||||||
if (serviceType === "generateObject") {
|
if (serviceType === 'generateObject') {
|
||||||
const lowerCaseMessage = cleanMessage.toLowerCase();
|
const lowerCaseMessage = cleanMessage.toLowerCase();
|
||||||
if (
|
if (
|
||||||
lowerCaseMessage.includes(
|
lowerCaseMessage.includes(
|
||||||
"no endpoints found that support tool use"
|
'no endpoints found that support tool use'
|
||||||
) ||
|
) ||
|
||||||
lowerCaseMessage.includes("does not support tool_use") ||
|
lowerCaseMessage.includes('does not support tool_use') ||
|
||||||
lowerCaseMessage.includes("tool use is not supported") ||
|
lowerCaseMessage.includes('tool use is not supported') ||
|
||||||
lowerCaseMessage.includes("tools are not supported") ||
|
lowerCaseMessage.includes('tools are not supported') ||
|
||||||
lowerCaseMessage.includes("function calling is not supported") ||
|
lowerCaseMessage.includes('function calling is not supported') ||
|
||||||
lowerCaseMessage.includes("tool use is not supported")
|
lowerCaseMessage.includes('tool use is not supported')
|
||||||
) {
|
) {
|
||||||
const specificErrorMsg = `Model '${modelId || "unknown"}' via provider '${providerName || "unknown"}' does not support the 'tool use' required by generateObjectService. Please configure a model that supports tool/function calling for the '${currentRole}' role, or use generateTextService if structured output is not strictly required.`;
|
const specificErrorMsg = `Model '${modelId || 'unknown'}' via provider '${providerName || 'unknown'}' does not support the 'tool use' required by generateObjectService. Please configure a model that supports tool/function calling for the '${currentRole}' role, or use generateTextService if structured output is not strictly required.`;
|
||||||
log("error", `[Tool Support Error] ${specificErrorMsg}`);
|
log('error', `[Tool Support Error] ${specificErrorMsg}`);
|
||||||
throw new Error(specificErrorMsg);
|
throw new Error(specificErrorMsg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log("error", `All roles in the sequence [${sequence.join(", ")}] failed.`);
|
log('error', `All roles in the sequence [${sequence.join(', ')}] failed.`);
|
||||||
throw new Error(lastCleanErrorMessage);
|
throw new Error(lastCleanErrorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -608,10 +608,10 @@ async function _unifiedServiceRunner(serviceType, params) {
|
|||||||
*/
|
*/
|
||||||
async function generateTextService(params) {
|
async function generateTextService(params) {
|
||||||
// Ensure default outputType if not provided
|
// Ensure default outputType if not provided
|
||||||
const defaults = { outputType: "cli" };
|
const defaults = { outputType: 'cli' };
|
||||||
const combinedParams = { ...defaults, ...params };
|
const combinedParams = { ...defaults, ...params };
|
||||||
// TODO: Validate commandName exists?
|
// TODO: Validate commandName exists?
|
||||||
return _unifiedServiceRunner("generateText", combinedParams);
|
return _unifiedServiceRunner('generateText', combinedParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -629,13 +629,13 @@ async function generateTextService(params) {
|
|||||||
* @returns {Promise<object>} Result object containing the stream and usage data.
|
* @returns {Promise<object>} Result object containing the stream and usage data.
|
||||||
*/
|
*/
|
||||||
async function streamTextService(params) {
|
async function streamTextService(params) {
|
||||||
const defaults = { outputType: "cli" };
|
const defaults = { outputType: 'cli' };
|
||||||
const combinedParams = { ...defaults, ...params };
|
const combinedParams = { ...defaults, ...params };
|
||||||
// TODO: Validate commandName exists?
|
// TODO: Validate commandName exists?
|
||||||
// NOTE: Telemetry for streaming might be tricky as usage data often comes at the end.
|
// NOTE: Telemetry for streaming might be tricky as usage data often comes at the end.
|
||||||
// The current implementation logs *after* the stream is returned.
|
// The current implementation logs *after* the stream is returned.
|
||||||
// We might need to adjust how usage is captured/logged for streams.
|
// We might need to adjust how usage is captured/logged for streams.
|
||||||
return _unifiedServiceRunner("streamText", combinedParams);
|
return _unifiedServiceRunner('streamText', combinedParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -657,13 +657,13 @@ async function streamTextService(params) {
|
|||||||
*/
|
*/
|
||||||
async function generateObjectService(params) {
|
async function generateObjectService(params) {
|
||||||
const defaults = {
|
const defaults = {
|
||||||
objectName: "generated_object",
|
objectName: 'generated_object',
|
||||||
maxRetries: 3,
|
maxRetries: 3,
|
||||||
outputType: "cli",
|
outputType: 'cli'
|
||||||
};
|
};
|
||||||
const combinedParams = { ...defaults, ...params };
|
const combinedParams = { ...defaults, ...params };
|
||||||
// TODO: Validate commandName exists?
|
// TODO: Validate commandName exists?
|
||||||
return _unifiedServiceRunner("generateObject", combinedParams);
|
return _unifiedServiceRunner('generateObject', combinedParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Telemetry Function ---
|
// --- Telemetry Function ---
|
||||||
@@ -685,10 +685,10 @@ async function logAiUsage({
|
|||||||
modelId,
|
modelId,
|
||||||
inputTokens,
|
inputTokens,
|
||||||
outputTokens,
|
outputTokens,
|
||||||
outputType,
|
outputType
|
||||||
}) {
|
}) {
|
||||||
try {
|
try {
|
||||||
const isMCP = outputType === "mcp";
|
const isMCP = outputType === 'mcp';
|
||||||
const timestamp = new Date().toISOString();
|
const timestamp = new Date().toISOString();
|
||||||
const totalTokens = (inputTokens || 0) + (outputTokens || 0);
|
const totalTokens = (inputTokens || 0) + (outputTokens || 0);
|
||||||
|
|
||||||
@@ -712,19 +712,19 @@ async function logAiUsage({
|
|||||||
outputTokens: outputTokens || 0,
|
outputTokens: outputTokens || 0,
|
||||||
totalTokens,
|
totalTokens,
|
||||||
totalCost: parseFloat(totalCost.toFixed(6)),
|
totalCost: parseFloat(totalCost.toFixed(6)),
|
||||||
currency, // Add currency to the telemetry data
|
currency // Add currency to the telemetry data
|
||||||
};
|
};
|
||||||
|
|
||||||
if (getDebugFlag()) {
|
if (getDebugFlag()) {
|
||||||
log("info", "AI Usage Telemetry:", telemetryData);
|
log('info', 'AI Usage Telemetry:', telemetryData);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO (Subtask 77.2): Send telemetryData securely to the external endpoint.
|
// TODO (Subtask 77.2): Send telemetryData securely to the external endpoint.
|
||||||
|
|
||||||
return telemetryData;
|
return telemetryData;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log("error", `Failed to log AI usage telemetry: ${error.message}`, {
|
log('error', `Failed to log AI usage telemetry: ${error.message}`, {
|
||||||
error,
|
error
|
||||||
});
|
});
|
||||||
// Don't re-throw; telemetry failure shouldn't block core functionality.
|
// Don't re-throw; telemetry failure shouldn't block core functionality.
|
||||||
return null;
|
return null;
|
||||||
@@ -735,5 +735,5 @@ export {
|
|||||||
generateTextService,
|
generateTextService,
|
||||||
streamTextService,
|
streamTextService,
|
||||||
generateObjectService,
|
generateObjectService,
|
||||||
logAiUsage,
|
logAiUsage
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
* Manages task dependencies and relationships
|
* Manages task dependencies and relationships
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import path from "path";
|
import path from 'path';
|
||||||
import chalk from "chalk";
|
import chalk from 'chalk';
|
||||||
import boxen from "boxen";
|
import boxen from 'boxen';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
log,
|
log,
|
||||||
@@ -14,12 +14,12 @@ import {
|
|||||||
taskExists,
|
taskExists,
|
||||||
formatTaskId,
|
formatTaskId,
|
||||||
findCycles,
|
findCycles,
|
||||||
isSilentMode,
|
isSilentMode
|
||||||
} from "./utils.js";
|
} from './utils.js';
|
||||||
|
|
||||||
import { displayBanner } from "./ui.js";
|
import { displayBanner } from './ui.js';
|
||||||
|
|
||||||
import { generateTaskFiles } from "./task-manager.js";
|
import { generateTaskFiles } from './task-manager.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a dependency to a task
|
* Add a dependency to a task
|
||||||
@@ -28,17 +28,17 @@ import { generateTaskFiles } from "./task-manager.js";
|
|||||||
* @param {number|string} dependencyId - ID of the task to add as dependency
|
* @param {number|string} dependencyId - ID of the task to add as dependency
|
||||||
*/
|
*/
|
||||||
async function addDependency(tasksPath, taskId, dependencyId) {
|
async function addDependency(tasksPath, taskId, dependencyId) {
|
||||||
log("info", `Adding dependency ${dependencyId} to task ${taskId}...`);
|
log('info', `Adding dependency ${dependencyId} to task ${taskId}...`);
|
||||||
|
|
||||||
const data = readJSON(tasksPath);
|
const data = readJSON(tasksPath);
|
||||||
if (!data || !data.tasks) {
|
if (!data || !data.tasks) {
|
||||||
log("error", "No valid tasks found in tasks.json");
|
log('error', 'No valid tasks found in tasks.json');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format the task and dependency IDs correctly
|
// Format the task and dependency IDs correctly
|
||||||
const formattedTaskId =
|
const formattedTaskId =
|
||||||
typeof taskId === "string" && taskId.includes(".")
|
typeof taskId === 'string' && taskId.includes('.')
|
||||||
? taskId
|
? taskId
|
||||||
: parseInt(taskId, 10);
|
: parseInt(taskId, 10);
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ async function addDependency(tasksPath, taskId, dependencyId) {
|
|||||||
// Check if the dependency task or subtask actually exists
|
// Check if the dependency task or subtask actually exists
|
||||||
if (!taskExists(data.tasks, formattedDependencyId)) {
|
if (!taskExists(data.tasks, formattedDependencyId)) {
|
||||||
log(
|
log(
|
||||||
"error",
|
'error',
|
||||||
`Dependency target ${formattedDependencyId} does not exist in tasks.json`
|
`Dependency target ${formattedDependencyId} does not exist in tasks.json`
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
@@ -57,20 +57,20 @@ async function addDependency(tasksPath, taskId, dependencyId) {
|
|||||||
let targetTask = null;
|
let targetTask = null;
|
||||||
let isSubtask = false;
|
let isSubtask = false;
|
||||||
|
|
||||||
if (typeof formattedTaskId === "string" && formattedTaskId.includes(".")) {
|
if (typeof formattedTaskId === 'string' && formattedTaskId.includes('.')) {
|
||||||
// Handle dot notation for subtasks (e.g., "1.2")
|
// Handle dot notation for subtasks (e.g., "1.2")
|
||||||
const [parentId, subtaskId] = formattedTaskId
|
const [parentId, subtaskId] = formattedTaskId
|
||||||
.split(".")
|
.split('.')
|
||||||
.map((id) => parseInt(id, 10));
|
.map((id) => parseInt(id, 10));
|
||||||
const parentTask = data.tasks.find((t) => t.id === parentId);
|
const parentTask = data.tasks.find((t) => t.id === parentId);
|
||||||
|
|
||||||
if (!parentTask) {
|
if (!parentTask) {
|
||||||
log("error", `Parent task ${parentId} not found.`);
|
log('error', `Parent task ${parentId} not found.`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!parentTask.subtasks) {
|
if (!parentTask.subtasks) {
|
||||||
log("error", `Parent task ${parentId} has no subtasks.`);
|
log('error', `Parent task ${parentId} has no subtasks.`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ async function addDependency(tasksPath, taskId, dependencyId) {
|
|||||||
isSubtask = true;
|
isSubtask = true;
|
||||||
|
|
||||||
if (!targetTask) {
|
if (!targetTask) {
|
||||||
log("error", `Subtask ${formattedTaskId} not found.`);
|
log('error', `Subtask ${formattedTaskId} not found.`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -86,7 +86,7 @@ async function addDependency(tasksPath, taskId, dependencyId) {
|
|||||||
targetTask = data.tasks.find((t) => t.id === formattedTaskId);
|
targetTask = data.tasks.find((t) => t.id === formattedTaskId);
|
||||||
|
|
||||||
if (!targetTask) {
|
if (!targetTask) {
|
||||||
log("error", `Task ${formattedTaskId} not found.`);
|
log('error', `Task ${formattedTaskId} not found.`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,7 +104,7 @@ async function addDependency(tasksPath, taskId, dependencyId) {
|
|||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
log(
|
log(
|
||||||
"warn",
|
'warn',
|
||||||
`Dependency ${formattedDependencyId} already exists in task ${formattedTaskId}.`
|
`Dependency ${formattedDependencyId} already exists in task ${formattedTaskId}.`
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@@ -112,7 +112,7 @@ async function addDependency(tasksPath, taskId, dependencyId) {
|
|||||||
|
|
||||||
// Check if the task is trying to depend on itself - compare full IDs (including subtask parts)
|
// Check if the task is trying to depend on itself - compare full IDs (including subtask parts)
|
||||||
if (String(formattedTaskId) === String(formattedDependencyId)) {
|
if (String(formattedTaskId) === String(formattedDependencyId)) {
|
||||||
log("error", `Task ${formattedTaskId} cannot depend on itself.`);
|
log('error', `Task ${formattedTaskId} cannot depend on itself.`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,30 +121,30 @@ async function addDependency(tasksPath, taskId, dependencyId) {
|
|||||||
let isSelfDependency = false;
|
let isSelfDependency = false;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
typeof formattedTaskId === "string" &&
|
typeof formattedTaskId === 'string' &&
|
||||||
typeof formattedDependencyId === "string" &&
|
typeof formattedDependencyId === 'string' &&
|
||||||
formattedTaskId.includes(".") &&
|
formattedTaskId.includes('.') &&
|
||||||
formattedDependencyId.includes(".")
|
formattedDependencyId.includes('.')
|
||||||
) {
|
) {
|
||||||
const [taskParentId] = formattedTaskId.split(".");
|
const [taskParentId] = formattedTaskId.split('.');
|
||||||
const [depParentId] = formattedDependencyId.split(".");
|
const [depParentId] = formattedDependencyId.split('.');
|
||||||
|
|
||||||
// Only treat it as a self-dependency if both the parent ID and subtask ID are identical
|
// Only treat it as a self-dependency if both the parent ID and subtask ID are identical
|
||||||
isSelfDependency = formattedTaskId === formattedDependencyId;
|
isSelfDependency = formattedTaskId === formattedDependencyId;
|
||||||
|
|
||||||
// Log for debugging
|
// Log for debugging
|
||||||
log(
|
log(
|
||||||
"debug",
|
'debug',
|
||||||
`Adding dependency between subtasks: ${formattedTaskId} depends on ${formattedDependencyId}`
|
`Adding dependency between subtasks: ${formattedTaskId} depends on ${formattedDependencyId}`
|
||||||
);
|
);
|
||||||
log(
|
log(
|
||||||
"debug",
|
'debug',
|
||||||
`Parent IDs: ${taskParentId} and ${depParentId}, Self-dependency check: ${isSelfDependency}`
|
`Parent IDs: ${taskParentId} and ${depParentId}, Self-dependency check: ${isSelfDependency}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSelfDependency) {
|
if (isSelfDependency) {
|
||||||
log("error", `Subtask ${formattedTaskId} cannot depend on itself.`);
|
log('error', `Subtask ${formattedTaskId} cannot depend on itself.`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,13 +158,13 @@ async function addDependency(tasksPath, taskId, dependencyId) {
|
|||||||
|
|
||||||
// Sort dependencies numerically or by parent task ID first, then subtask ID
|
// Sort dependencies numerically or by parent task ID first, then subtask ID
|
||||||
targetTask.dependencies.sort((a, b) => {
|
targetTask.dependencies.sort((a, b) => {
|
||||||
if (typeof a === "number" && typeof b === "number") {
|
if (typeof a === 'number' && typeof b === 'number') {
|
||||||
return a - b;
|
return a - b;
|
||||||
} else if (typeof a === "string" && typeof b === "string") {
|
} else if (typeof a === 'string' && typeof b === 'string') {
|
||||||
const [aParent, aChild] = a.split(".").map(Number);
|
const [aParent, aChild] = a.split('.').map(Number);
|
||||||
const [bParent, bChild] = b.split(".").map(Number);
|
const [bParent, bChild] = b.split('.').map(Number);
|
||||||
return aParent !== bParent ? aParent - bParent : aChild - bChild;
|
return aParent !== bParent ? aParent - bParent : aChild - bChild;
|
||||||
} else if (typeof a === "number") {
|
} else if (typeof a === 'number') {
|
||||||
return -1; // Numbers come before strings
|
return -1; // Numbers come before strings
|
||||||
} else {
|
} else {
|
||||||
return 1; // Strings come after numbers
|
return 1; // Strings come after numbers
|
||||||
@@ -174,7 +174,7 @@ async function addDependency(tasksPath, taskId, dependencyId) {
|
|||||||
// Save changes
|
// Save changes
|
||||||
writeJSON(tasksPath, data);
|
writeJSON(tasksPath, data);
|
||||||
log(
|
log(
|
||||||
"success",
|
'success',
|
||||||
`Added dependency ${formattedDependencyId} to task ${formattedTaskId}`
|
`Added dependency ${formattedDependencyId} to task ${formattedTaskId}`
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -186,9 +186,9 @@ async function addDependency(tasksPath, taskId, dependencyId) {
|
|||||||
`Task ${chalk.bold(formattedTaskId)} now depends on ${chalk.bold(formattedDependencyId)}`,
|
`Task ${chalk.bold(formattedTaskId)} now depends on ${chalk.bold(formattedDependencyId)}`,
|
||||||
{
|
{
|
||||||
padding: 1,
|
padding: 1,
|
||||||
borderColor: "green",
|
borderColor: 'green',
|
||||||
borderStyle: "round",
|
borderStyle: 'round',
|
||||||
margin: { top: 1 },
|
margin: { top: 1 }
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -197,10 +197,10 @@ async function addDependency(tasksPath, taskId, dependencyId) {
|
|||||||
// Generate updated task files
|
// Generate updated task files
|
||||||
await generateTaskFiles(tasksPath, path.dirname(tasksPath));
|
await generateTaskFiles(tasksPath, path.dirname(tasksPath));
|
||||||
|
|
||||||
log("info", "Task files regenerated with updated dependencies.");
|
log('info', 'Task files regenerated with updated dependencies.');
|
||||||
} else {
|
} else {
|
||||||
log(
|
log(
|
||||||
"error",
|
'error',
|
||||||
`Cannot add dependency ${formattedDependencyId} to task ${formattedTaskId} as it would create a circular dependency.`
|
`Cannot add dependency ${formattedDependencyId} to task ${formattedTaskId} as it would create a circular dependency.`
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
@@ -214,18 +214,18 @@ async function addDependency(tasksPath, taskId, dependencyId) {
|
|||||||
* @param {number|string} dependencyId - ID of the task to remove as dependency
|
* @param {number|string} dependencyId - ID of the task to remove as dependency
|
||||||
*/
|
*/
|
||||||
async function removeDependency(tasksPath, taskId, dependencyId) {
|
async function removeDependency(tasksPath, taskId, dependencyId) {
|
||||||
log("info", `Removing dependency ${dependencyId} from task ${taskId}...`);
|
log('info', `Removing dependency ${dependencyId} from task ${taskId}...`);
|
||||||
|
|
||||||
// Read tasks file
|
// Read tasks file
|
||||||
const data = readJSON(tasksPath);
|
const data = readJSON(tasksPath);
|
||||||
if (!data || !data.tasks) {
|
if (!data || !data.tasks) {
|
||||||
log("error", "No valid tasks found.");
|
log('error', 'No valid tasks found.');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format the task and dependency IDs correctly
|
// Format the task and dependency IDs correctly
|
||||||
const formattedTaskId =
|
const formattedTaskId =
|
||||||
typeof taskId === "string" && taskId.includes(".")
|
typeof taskId === 'string' && taskId.includes('.')
|
||||||
? taskId
|
? taskId
|
||||||
: parseInt(taskId, 10);
|
: parseInt(taskId, 10);
|
||||||
|
|
||||||
@@ -235,20 +235,20 @@ async function removeDependency(tasksPath, taskId, dependencyId) {
|
|||||||
let targetTask = null;
|
let targetTask = null;
|
||||||
let isSubtask = false;
|
let isSubtask = false;
|
||||||
|
|
||||||
if (typeof formattedTaskId === "string" && formattedTaskId.includes(".")) {
|
if (typeof formattedTaskId === 'string' && formattedTaskId.includes('.')) {
|
||||||
// Handle dot notation for subtasks (e.g., "1.2")
|
// Handle dot notation for subtasks (e.g., "1.2")
|
||||||
const [parentId, subtaskId] = formattedTaskId
|
const [parentId, subtaskId] = formattedTaskId
|
||||||
.split(".")
|
.split('.')
|
||||||
.map((id) => parseInt(id, 10));
|
.map((id) => parseInt(id, 10));
|
||||||
const parentTask = data.tasks.find((t) => t.id === parentId);
|
const parentTask = data.tasks.find((t) => t.id === parentId);
|
||||||
|
|
||||||
if (!parentTask) {
|
if (!parentTask) {
|
||||||
log("error", `Parent task ${parentId} not found.`);
|
log('error', `Parent task ${parentId} not found.`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!parentTask.subtasks) {
|
if (!parentTask.subtasks) {
|
||||||
log("error", `Parent task ${parentId} has no subtasks.`);
|
log('error', `Parent task ${parentId} has no subtasks.`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,7 +256,7 @@ async function removeDependency(tasksPath, taskId, dependencyId) {
|
|||||||
isSubtask = true;
|
isSubtask = true;
|
||||||
|
|
||||||
if (!targetTask) {
|
if (!targetTask) {
|
||||||
log("error", `Subtask ${formattedTaskId} not found.`);
|
log('error', `Subtask ${formattedTaskId} not found.`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -264,7 +264,7 @@ async function removeDependency(tasksPath, taskId, dependencyId) {
|
|||||||
targetTask = data.tasks.find((t) => t.id === formattedTaskId);
|
targetTask = data.tasks.find((t) => t.id === formattedTaskId);
|
||||||
|
|
||||||
if (!targetTask) {
|
if (!targetTask) {
|
||||||
log("error", `Task ${formattedTaskId} not found.`);
|
log('error', `Task ${formattedTaskId} not found.`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -272,7 +272,7 @@ async function removeDependency(tasksPath, taskId, dependencyId) {
|
|||||||
// Check if the task has any dependencies
|
// Check if the task has any dependencies
|
||||||
if (!targetTask.dependencies || targetTask.dependencies.length === 0) {
|
if (!targetTask.dependencies || targetTask.dependencies.length === 0) {
|
||||||
log(
|
log(
|
||||||
"info",
|
'info',
|
||||||
`Task ${formattedTaskId} has no dependencies, nothing to remove.`
|
`Task ${formattedTaskId} has no dependencies, nothing to remove.`
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@@ -287,10 +287,10 @@ async function removeDependency(tasksPath, taskId, dependencyId) {
|
|||||||
let depStr = String(dep);
|
let depStr = String(dep);
|
||||||
|
|
||||||
// Special handling for numeric IDs that might be subtask references
|
// Special handling for numeric IDs that might be subtask references
|
||||||
if (typeof dep === "number" && dep < 100 && isSubtask) {
|
if (typeof dep === 'number' && dep < 100 && isSubtask) {
|
||||||
// It's likely a reference to another subtask in the same parent task
|
// It's likely a reference to another subtask in the same parent task
|
||||||
// Convert to full format for comparison (e.g., 2 -> "1.2" for a subtask in task 1)
|
// Convert to full format for comparison (e.g., 2 -> "1.2" for a subtask in task 1)
|
||||||
const [parentId] = formattedTaskId.split(".");
|
const [parentId] = formattedTaskId.split('.');
|
||||||
depStr = `${parentId}.${dep}`;
|
depStr = `${parentId}.${dep}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,7 +299,7 @@ async function removeDependency(tasksPath, taskId, dependencyId) {
|
|||||||
|
|
||||||
if (dependencyIndex === -1) {
|
if (dependencyIndex === -1) {
|
||||||
log(
|
log(
|
||||||
"info",
|
'info',
|
||||||
`Task ${formattedTaskId} does not depend on ${formattedDependencyId}, no changes made.`
|
`Task ${formattedTaskId} does not depend on ${formattedDependencyId}, no changes made.`
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@@ -313,7 +313,7 @@ async function removeDependency(tasksPath, taskId, dependencyId) {
|
|||||||
|
|
||||||
// Success message
|
// Success message
|
||||||
log(
|
log(
|
||||||
"success",
|
'success',
|
||||||
`Removed dependency: Task ${formattedTaskId} no longer depends on ${formattedDependencyId}`
|
`Removed dependency: Task ${formattedTaskId} no longer depends on ${formattedDependencyId}`
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -325,9 +325,9 @@ async function removeDependency(tasksPath, taskId, dependencyId) {
|
|||||||
`Task ${chalk.bold(formattedTaskId)} no longer depends on ${chalk.bold(formattedDependencyId)}`,
|
`Task ${chalk.bold(formattedTaskId)} no longer depends on ${chalk.bold(formattedDependencyId)}`,
|
||||||
{
|
{
|
||||||
padding: 1,
|
padding: 1,
|
||||||
borderColor: "green",
|
borderColor: 'green',
|
||||||
borderStyle: "round",
|
borderStyle: 'round',
|
||||||
margin: { top: 1 },
|
margin: { top: 1 }
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -358,8 +358,8 @@ function isCircularDependency(tasks, taskId, chain = []) {
|
|||||||
let parentIdForSubtask = null;
|
let parentIdForSubtask = null;
|
||||||
|
|
||||||
// Check if this is a subtask reference (e.g., "1.2")
|
// Check if this is a subtask reference (e.g., "1.2")
|
||||||
if (taskIdStr.includes(".")) {
|
if (taskIdStr.includes('.')) {
|
||||||
const [parentId, subtaskId] = taskIdStr.split(".").map(Number);
|
const [parentId, subtaskId] = taskIdStr.split('.').map(Number);
|
||||||
const parentTask = tasks.find((t) => t.id === parentId);
|
const parentTask = tasks.find((t) => t.id === parentId);
|
||||||
parentIdForSubtask = parentId; // Store parent ID if it's a subtask
|
parentIdForSubtask = parentId; // Store parent ID if it's a subtask
|
||||||
|
|
||||||
@@ -385,7 +385,7 @@ function isCircularDependency(tasks, taskId, chain = []) {
|
|||||||
return task.dependencies.some((depId) => {
|
return task.dependencies.some((depId) => {
|
||||||
let normalizedDepId = String(depId);
|
let normalizedDepId = String(depId);
|
||||||
// Normalize relative subtask dependencies
|
// Normalize relative subtask dependencies
|
||||||
if (typeof depId === "number" && parentIdForSubtask !== null) {
|
if (typeof depId === 'number' && parentIdForSubtask !== null) {
|
||||||
// If the current task is a subtask AND the dependency is a number,
|
// If the current task is a subtask AND the dependency is a number,
|
||||||
// assume it refers to a sibling subtask.
|
// assume it refers to a sibling subtask.
|
||||||
normalizedDepId = `${parentIdForSubtask}.${depId}`;
|
normalizedDepId = `${parentIdForSubtask}.${depId}`;
|
||||||
@@ -413,9 +413,9 @@ function validateTaskDependencies(tasks) {
|
|||||||
// Check for self-dependencies
|
// Check for self-dependencies
|
||||||
if (String(depId) === String(task.id)) {
|
if (String(depId) === String(task.id)) {
|
||||||
issues.push({
|
issues.push({
|
||||||
type: "self",
|
type: 'self',
|
||||||
taskId: task.id,
|
taskId: task.id,
|
||||||
message: `Task ${task.id} depends on itself`,
|
message: `Task ${task.id} depends on itself`
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -423,10 +423,10 @@ function validateTaskDependencies(tasks) {
|
|||||||
// Check if dependency exists
|
// Check if dependency exists
|
||||||
if (!taskExists(tasks, depId)) {
|
if (!taskExists(tasks, depId)) {
|
||||||
issues.push({
|
issues.push({
|
||||||
type: "missing",
|
type: 'missing',
|
||||||
taskId: task.id,
|
taskId: task.id,
|
||||||
dependencyId: depId,
|
dependencyId: depId,
|
||||||
message: `Task ${task.id} depends on non-existent task ${depId}`,
|
message: `Task ${task.id} depends on non-existent task ${depId}`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -434,9 +434,9 @@ function validateTaskDependencies(tasks) {
|
|||||||
// Check for circular dependencies
|
// Check for circular dependencies
|
||||||
if (isCircularDependency(tasks, task.id)) {
|
if (isCircularDependency(tasks, task.id)) {
|
||||||
issues.push({
|
issues.push({
|
||||||
type: "circular",
|
type: 'circular',
|
||||||
taskId: task.id,
|
taskId: task.id,
|
||||||
message: `Task ${task.id} is part of a circular dependency chain`,
|
message: `Task ${task.id} is part of a circular dependency chain`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -454,12 +454,12 @@ function validateTaskDependencies(tasks) {
|
|||||||
// Check for self-dependencies in subtasks
|
// Check for self-dependencies in subtasks
|
||||||
if (
|
if (
|
||||||
String(depId) === String(fullSubtaskId) ||
|
String(depId) === String(fullSubtaskId) ||
|
||||||
(typeof depId === "number" && depId === subtask.id)
|
(typeof depId === 'number' && depId === subtask.id)
|
||||||
) {
|
) {
|
||||||
issues.push({
|
issues.push({
|
||||||
type: "self",
|
type: 'self',
|
||||||
taskId: fullSubtaskId,
|
taskId: fullSubtaskId,
|
||||||
message: `Subtask ${fullSubtaskId} depends on itself`,
|
message: `Subtask ${fullSubtaskId} depends on itself`
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -467,10 +467,10 @@ function validateTaskDependencies(tasks) {
|
|||||||
// Check if dependency exists
|
// Check if dependency exists
|
||||||
if (!taskExists(tasks, depId)) {
|
if (!taskExists(tasks, depId)) {
|
||||||
issues.push({
|
issues.push({
|
||||||
type: "missing",
|
type: 'missing',
|
||||||
taskId: fullSubtaskId,
|
taskId: fullSubtaskId,
|
||||||
dependencyId: depId,
|
dependencyId: depId,
|
||||||
message: `Subtask ${fullSubtaskId} depends on non-existent task/subtask ${depId}`,
|
message: `Subtask ${fullSubtaskId} depends on non-existent task/subtask ${depId}`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -478,9 +478,9 @@ function validateTaskDependencies(tasks) {
|
|||||||
// Check for circular dependencies in subtasks
|
// Check for circular dependencies in subtasks
|
||||||
if (isCircularDependency(tasks, fullSubtaskId)) {
|
if (isCircularDependency(tasks, fullSubtaskId)) {
|
||||||
issues.push({
|
issues.push({
|
||||||
type: "circular",
|
type: 'circular',
|
||||||
taskId: fullSubtaskId,
|
taskId: fullSubtaskId,
|
||||||
message: `Subtask ${fullSubtaskId} is part of a circular dependency chain`,
|
message: `Subtask ${fullSubtaskId} is part of a circular dependency chain`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -489,7 +489,7 @@ function validateTaskDependencies(tasks) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
valid: issues.length === 0,
|
valid: issues.length === 0,
|
||||||
issues,
|
issues
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -508,13 +508,13 @@ function removeDuplicateDependencies(tasksData) {
|
|||||||
const uniqueDeps = [...new Set(task.dependencies)];
|
const uniqueDeps = [...new Set(task.dependencies)];
|
||||||
return {
|
return {
|
||||||
...task,
|
...task,
|
||||||
dependencies: uniqueDeps,
|
dependencies: uniqueDeps
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...tasksData,
|
...tasksData,
|
||||||
tasks,
|
tasks
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -554,7 +554,7 @@ function cleanupSubtaskDependencies(tasksData) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...tasksData,
|
...tasksData,
|
||||||
tasks,
|
tasks
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -563,12 +563,12 @@ function cleanupSubtaskDependencies(tasksData) {
|
|||||||
* @param {string} tasksPath - Path to tasks.json
|
* @param {string} tasksPath - Path to tasks.json
|
||||||
*/
|
*/
|
||||||
async function validateDependenciesCommand(tasksPath, options = {}) {
|
async function validateDependenciesCommand(tasksPath, options = {}) {
|
||||||
log("info", "Checking for invalid dependencies in task files...");
|
log('info', 'Checking for invalid dependencies in task files...');
|
||||||
|
|
||||||
// Read tasks data
|
// Read tasks data
|
||||||
const data = readJSON(tasksPath);
|
const data = readJSON(tasksPath);
|
||||||
if (!data || !data.tasks) {
|
if (!data || !data.tasks) {
|
||||||
log("error", "No valid tasks found in tasks.json");
|
log('error', 'No valid tasks found in tasks.json');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -582,7 +582,7 @@ async function validateDependenciesCommand(tasksPath, options = {}) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
log(
|
log(
|
||||||
"info",
|
'info',
|
||||||
`Analyzing dependencies for ${taskCount} tasks and ${subtaskCount} subtasks...`
|
`Analyzing dependencies for ${taskCount} tasks and ${subtaskCount} subtasks...`
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -592,7 +592,7 @@ async function validateDependenciesCommand(tasksPath, options = {}) {
|
|||||||
|
|
||||||
if (!validationResult.valid) {
|
if (!validationResult.valid) {
|
||||||
log(
|
log(
|
||||||
"error",
|
'error',
|
||||||
`Dependency validation failed. Found ${validationResult.issues.length} issue(s):`
|
`Dependency validation failed. Found ${validationResult.issues.length} issue(s):`
|
||||||
);
|
);
|
||||||
validationResult.issues.forEach((issue) => {
|
validationResult.issues.forEach((issue) => {
|
||||||
@@ -600,7 +600,7 @@ async function validateDependenciesCommand(tasksPath, options = {}) {
|
|||||||
if (issue.dependencyId) {
|
if (issue.dependencyId) {
|
||||||
errorMsg += ` (Dependency: ${issue.dependencyId})`;
|
errorMsg += ` (Dependency: ${issue.dependencyId})`;
|
||||||
}
|
}
|
||||||
log("error", errorMsg); // Log each issue as an error
|
log('error', errorMsg); // Log each issue as an error
|
||||||
});
|
});
|
||||||
|
|
||||||
// Optionally exit if validation fails, depending on desired behavior
|
// Optionally exit if validation fails, depending on desired behavior
|
||||||
@@ -611,22 +611,22 @@ async function validateDependenciesCommand(tasksPath, options = {}) {
|
|||||||
console.log(
|
console.log(
|
||||||
boxen(
|
boxen(
|
||||||
chalk.red(`Dependency Validation FAILED\n\n`) +
|
chalk.red(`Dependency Validation FAILED\n\n`) +
|
||||||
`${chalk.cyan("Tasks checked:")} ${taskCount}\n` +
|
`${chalk.cyan('Tasks checked:')} ${taskCount}\n` +
|
||||||
`${chalk.cyan("Subtasks checked:")} ${subtaskCount}\n` +
|
`${chalk.cyan('Subtasks checked:')} ${subtaskCount}\n` +
|
||||||
`${chalk.red("Issues found:")} ${validationResult.issues.length}`, // Display count from result
|
`${chalk.red('Issues found:')} ${validationResult.issues.length}`, // Display count from result
|
||||||
{
|
{
|
||||||
padding: 1,
|
padding: 1,
|
||||||
borderColor: "red",
|
borderColor: 'red',
|
||||||
borderStyle: "round",
|
borderStyle: 'round',
|
||||||
margin: { top: 1, bottom: 1 },
|
margin: { top: 1, bottom: 1 }
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log(
|
log(
|
||||||
"success",
|
'success',
|
||||||
"No invalid dependencies found - all dependencies are valid"
|
'No invalid dependencies found - all dependencies are valid'
|
||||||
);
|
);
|
||||||
|
|
||||||
// Show validation summary - only if not in silent mode
|
// Show validation summary - only if not in silent mode
|
||||||
@@ -634,21 +634,21 @@ async function validateDependenciesCommand(tasksPath, options = {}) {
|
|||||||
console.log(
|
console.log(
|
||||||
boxen(
|
boxen(
|
||||||
chalk.green(`All Dependencies Are Valid\n\n`) +
|
chalk.green(`All Dependencies Are Valid\n\n`) +
|
||||||
`${chalk.cyan("Tasks checked:")} ${taskCount}\n` +
|
`${chalk.cyan('Tasks checked:')} ${taskCount}\n` +
|
||||||
`${chalk.cyan("Subtasks checked:")} ${subtaskCount}\n` +
|
`${chalk.cyan('Subtasks checked:')} ${subtaskCount}\n` +
|
||||||
`${chalk.cyan("Total dependencies verified:")} ${countAllDependencies(data.tasks)}`,
|
`${chalk.cyan('Total dependencies verified:')} ${countAllDependencies(data.tasks)}`,
|
||||||
{
|
{
|
||||||
padding: 1,
|
padding: 1,
|
||||||
borderColor: "green",
|
borderColor: 'green',
|
||||||
borderStyle: "round",
|
borderStyle: 'round',
|
||||||
margin: { top: 1, bottom: 1 },
|
margin: { top: 1, bottom: 1 }
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log("error", "Error validating dependencies:", error);
|
log('error', 'Error validating dependencies:', error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -686,13 +686,13 @@ function countAllDependencies(tasks) {
|
|||||||
* @param {Object} options - Options object
|
* @param {Object} options - Options object
|
||||||
*/
|
*/
|
||||||
async function fixDependenciesCommand(tasksPath, options = {}) {
|
async function fixDependenciesCommand(tasksPath, options = {}) {
|
||||||
log("info", "Checking for and fixing invalid dependencies in tasks.json...");
|
log('info', 'Checking for and fixing invalid dependencies in tasks.json...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Read tasks data
|
// Read tasks data
|
||||||
const data = readJSON(tasksPath);
|
const data = readJSON(tasksPath);
|
||||||
if (!data || !data.tasks) {
|
if (!data || !data.tasks) {
|
||||||
log("error", "No valid tasks found in tasks.json");
|
log('error', 'No valid tasks found in tasks.json');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -706,7 +706,7 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
|
|||||||
duplicateDependenciesRemoved: 0,
|
duplicateDependenciesRemoved: 0,
|
||||||
circularDependenciesFixed: 0,
|
circularDependenciesFixed: 0,
|
||||||
tasksFixed: 0,
|
tasksFixed: 0,
|
||||||
subtasksFixed: 0,
|
subtasksFixed: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
// First phase: Remove duplicate dependencies in tasks
|
// First phase: Remove duplicate dependencies in tasks
|
||||||
@@ -718,7 +718,7 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
|
|||||||
const depIdStr = String(depId);
|
const depIdStr = String(depId);
|
||||||
if (uniqueDeps.has(depIdStr)) {
|
if (uniqueDeps.has(depIdStr)) {
|
||||||
log(
|
log(
|
||||||
"info",
|
'info',
|
||||||
`Removing duplicate dependency from task ${task.id}: ${depId}`
|
`Removing duplicate dependency from task ${task.id}: ${depId}`
|
||||||
);
|
);
|
||||||
stats.duplicateDependenciesRemoved++;
|
stats.duplicateDependenciesRemoved++;
|
||||||
@@ -740,12 +740,12 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
|
|||||||
const originalLength = subtask.dependencies.length;
|
const originalLength = subtask.dependencies.length;
|
||||||
subtask.dependencies = subtask.dependencies.filter((depId) => {
|
subtask.dependencies = subtask.dependencies.filter((depId) => {
|
||||||
let depIdStr = String(depId);
|
let depIdStr = String(depId);
|
||||||
if (typeof depId === "number" && depId < 100) {
|
if (typeof depId === 'number' && depId < 100) {
|
||||||
depIdStr = `${task.id}.${depId}`;
|
depIdStr = `${task.id}.${depId}`;
|
||||||
}
|
}
|
||||||
if (uniqueDeps.has(depIdStr)) {
|
if (uniqueDeps.has(depIdStr)) {
|
||||||
log(
|
log(
|
||||||
"info",
|
'info',
|
||||||
`Removing duplicate dependency from subtask ${task.id}.${subtask.id}: ${depId}`
|
`Removing duplicate dependency from subtask ${task.id}.${subtask.id}: ${depId}`
|
||||||
);
|
);
|
||||||
stats.duplicateDependenciesRemoved++;
|
stats.duplicateDependenciesRemoved++;
|
||||||
@@ -778,13 +778,13 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
|
|||||||
if (task.dependencies && Array.isArray(task.dependencies)) {
|
if (task.dependencies && Array.isArray(task.dependencies)) {
|
||||||
const originalLength = task.dependencies.length;
|
const originalLength = task.dependencies.length;
|
||||||
task.dependencies = task.dependencies.filter((depId) => {
|
task.dependencies = task.dependencies.filter((depId) => {
|
||||||
const isSubtask = typeof depId === "string" && depId.includes(".");
|
const isSubtask = typeof depId === 'string' && depId.includes('.');
|
||||||
|
|
||||||
if (isSubtask) {
|
if (isSubtask) {
|
||||||
// Check if the subtask exists
|
// Check if the subtask exists
|
||||||
if (!validSubtaskIds.has(depId)) {
|
if (!validSubtaskIds.has(depId)) {
|
||||||
log(
|
log(
|
||||||
"info",
|
'info',
|
||||||
`Removing invalid subtask dependency from task ${task.id}: ${depId} (subtask does not exist)`
|
`Removing invalid subtask dependency from task ${task.id}: ${depId} (subtask does not exist)`
|
||||||
);
|
);
|
||||||
stats.nonExistentDependenciesRemoved++;
|
stats.nonExistentDependenciesRemoved++;
|
||||||
@@ -794,10 +794,10 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
|
|||||||
} else {
|
} else {
|
||||||
// Check if the task exists
|
// Check if the task exists
|
||||||
const numericId =
|
const numericId =
|
||||||
typeof depId === "string" ? parseInt(depId, 10) : depId;
|
typeof depId === 'string' ? parseInt(depId, 10) : depId;
|
||||||
if (!validTaskIds.has(numericId)) {
|
if (!validTaskIds.has(numericId)) {
|
||||||
log(
|
log(
|
||||||
"info",
|
'info',
|
||||||
`Removing invalid task dependency from task ${task.id}: ${depId} (task does not exist)`
|
`Removing invalid task dependency from task ${task.id}: ${depId} (task does not exist)`
|
||||||
);
|
);
|
||||||
stats.nonExistentDependenciesRemoved++;
|
stats.nonExistentDependenciesRemoved++;
|
||||||
@@ -821,9 +821,9 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
|
|||||||
|
|
||||||
// First check for self-dependencies
|
// First check for self-dependencies
|
||||||
const hasSelfDependency = subtask.dependencies.some((depId) => {
|
const hasSelfDependency = subtask.dependencies.some((depId) => {
|
||||||
if (typeof depId === "string" && depId.includes(".")) {
|
if (typeof depId === 'string' && depId.includes('.')) {
|
||||||
return depId === subtaskId;
|
return depId === subtaskId;
|
||||||
} else if (typeof depId === "number" && depId < 100) {
|
} else if (typeof depId === 'number' && depId < 100) {
|
||||||
return depId === subtask.id;
|
return depId === subtask.id;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -832,13 +832,13 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
|
|||||||
if (hasSelfDependency) {
|
if (hasSelfDependency) {
|
||||||
subtask.dependencies = subtask.dependencies.filter((depId) => {
|
subtask.dependencies = subtask.dependencies.filter((depId) => {
|
||||||
const normalizedDepId =
|
const normalizedDepId =
|
||||||
typeof depId === "number" && depId < 100
|
typeof depId === 'number' && depId < 100
|
||||||
? `${task.id}.${depId}`
|
? `${task.id}.${depId}`
|
||||||
: String(depId);
|
: String(depId);
|
||||||
|
|
||||||
if (normalizedDepId === subtaskId) {
|
if (normalizedDepId === subtaskId) {
|
||||||
log(
|
log(
|
||||||
"info",
|
'info',
|
||||||
`Removing self-dependency from subtask ${subtaskId}`
|
`Removing self-dependency from subtask ${subtaskId}`
|
||||||
);
|
);
|
||||||
stats.selfDependenciesRemoved++;
|
stats.selfDependenciesRemoved++;
|
||||||
@@ -850,10 +850,10 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
|
|||||||
|
|
||||||
// Then check for non-existent dependencies
|
// Then check for non-existent dependencies
|
||||||
subtask.dependencies = subtask.dependencies.filter((depId) => {
|
subtask.dependencies = subtask.dependencies.filter((depId) => {
|
||||||
if (typeof depId === "string" && depId.includes(".")) {
|
if (typeof depId === 'string' && depId.includes('.')) {
|
||||||
if (!validSubtaskIds.has(depId)) {
|
if (!validSubtaskIds.has(depId)) {
|
||||||
log(
|
log(
|
||||||
"info",
|
'info',
|
||||||
`Removing invalid subtask dependency from subtask ${subtaskId}: ${depId} (subtask does not exist)`
|
`Removing invalid subtask dependency from subtask ${subtaskId}: ${depId} (subtask does not exist)`
|
||||||
);
|
);
|
||||||
stats.nonExistentDependenciesRemoved++;
|
stats.nonExistentDependenciesRemoved++;
|
||||||
@@ -864,7 +864,7 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
|
|||||||
|
|
||||||
// Handle numeric dependencies
|
// Handle numeric dependencies
|
||||||
const numericId =
|
const numericId =
|
||||||
typeof depId === "number" ? depId : parseInt(depId, 10);
|
typeof depId === 'number' ? depId : parseInt(depId, 10);
|
||||||
|
|
||||||
// Small numbers likely refer to subtasks in the same task
|
// Small numbers likely refer to subtasks in the same task
|
||||||
if (numericId < 100) {
|
if (numericId < 100) {
|
||||||
@@ -872,7 +872,7 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
|
|||||||
|
|
||||||
if (!validSubtaskIds.has(fullSubtaskId)) {
|
if (!validSubtaskIds.has(fullSubtaskId)) {
|
||||||
log(
|
log(
|
||||||
"info",
|
'info',
|
||||||
`Removing invalid subtask dependency from subtask ${subtaskId}: ${numericId}`
|
`Removing invalid subtask dependency from subtask ${subtaskId}: ${numericId}`
|
||||||
);
|
);
|
||||||
stats.nonExistentDependenciesRemoved++;
|
stats.nonExistentDependenciesRemoved++;
|
||||||
@@ -885,7 +885,7 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
|
|||||||
// Otherwise it's a task reference
|
// Otherwise it's a task reference
|
||||||
if (!validTaskIds.has(numericId)) {
|
if (!validTaskIds.has(numericId)) {
|
||||||
log(
|
log(
|
||||||
"info",
|
'info',
|
||||||
`Removing invalid task dependency from subtask ${subtaskId}: ${numericId}`
|
`Removing invalid task dependency from subtask ${subtaskId}: ${numericId}`
|
||||||
);
|
);
|
||||||
stats.nonExistentDependenciesRemoved++;
|
stats.nonExistentDependenciesRemoved++;
|
||||||
@@ -904,7 +904,7 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Third phase: Check for circular dependencies
|
// Third phase: Check for circular dependencies
|
||||||
log("info", "Checking for circular dependencies...");
|
log('info', 'Checking for circular dependencies...');
|
||||||
|
|
||||||
// Build the dependency map for subtasks
|
// Build the dependency map for subtasks
|
||||||
const subtaskDependencyMap = new Map();
|
const subtaskDependencyMap = new Map();
|
||||||
@@ -915,9 +915,9 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
|
|||||||
|
|
||||||
if (subtask.dependencies && Array.isArray(subtask.dependencies)) {
|
if (subtask.dependencies && Array.isArray(subtask.dependencies)) {
|
||||||
const normalizedDeps = subtask.dependencies.map((depId) => {
|
const normalizedDeps = subtask.dependencies.map((depId) => {
|
||||||
if (typeof depId === "string" && depId.includes(".")) {
|
if (typeof depId === 'string' && depId.includes('.')) {
|
||||||
return depId;
|
return depId;
|
||||||
} else if (typeof depId === "number" && depId < 100) {
|
} else if (typeof depId === 'number' && depId < 100) {
|
||||||
return `${task.id}.${depId}`;
|
return `${task.id}.${depId}`;
|
||||||
}
|
}
|
||||||
return String(depId);
|
return String(depId);
|
||||||
@@ -945,7 +945,7 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
|
|||||||
|
|
||||||
if (cycleEdges.length > 0) {
|
if (cycleEdges.length > 0) {
|
||||||
const [taskId, subtaskNum] = subtaskId
|
const [taskId, subtaskNum] = subtaskId
|
||||||
.split(".")
|
.split('.')
|
||||||
.map((part) => Number(part));
|
.map((part) => Number(part));
|
||||||
const task = data.tasks.find((t) => t.id === taskId);
|
const task = data.tasks.find((t) => t.id === taskId);
|
||||||
|
|
||||||
@@ -956,9 +956,9 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
|
|||||||
const originalLength = subtask.dependencies.length;
|
const originalLength = subtask.dependencies.length;
|
||||||
|
|
||||||
const edgesToRemove = cycleEdges.map((edge) => {
|
const edgesToRemove = cycleEdges.map((edge) => {
|
||||||
if (edge.includes(".")) {
|
if (edge.includes('.')) {
|
||||||
const [depTaskId, depSubtaskId] = edge
|
const [depTaskId, depSubtaskId] = edge
|
||||||
.split(".")
|
.split('.')
|
||||||
.map((part) => Number(part));
|
.map((part) => Number(part));
|
||||||
|
|
||||||
if (depTaskId === taskId) {
|
if (depTaskId === taskId) {
|
||||||
@@ -973,7 +973,7 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
|
|||||||
|
|
||||||
subtask.dependencies = subtask.dependencies.filter((depId) => {
|
subtask.dependencies = subtask.dependencies.filter((depId) => {
|
||||||
const normalizedDepId =
|
const normalizedDepId =
|
||||||
typeof depId === "number" && depId < 100
|
typeof depId === 'number' && depId < 100
|
||||||
? `${taskId}.${depId}`
|
? `${taskId}.${depId}`
|
||||||
: String(depId);
|
: String(depId);
|
||||||
|
|
||||||
@@ -982,7 +982,7 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
|
|||||||
edgesToRemove.includes(normalizedDepId)
|
edgesToRemove.includes(normalizedDepId)
|
||||||
) {
|
) {
|
||||||
log(
|
log(
|
||||||
"info",
|
'info',
|
||||||
`Breaking circular dependency: Removing ${normalizedDepId} from subtask ${subtaskId}`
|
`Breaking circular dependency: Removing ${normalizedDepId} from subtask ${subtaskId}`
|
||||||
);
|
);
|
||||||
stats.circularDependenciesFixed++;
|
stats.circularDependenciesFixed++;
|
||||||
@@ -1005,13 +1005,13 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
|
|||||||
if (dataChanged) {
|
if (dataChanged) {
|
||||||
// Save the changes
|
// Save the changes
|
||||||
writeJSON(tasksPath, data);
|
writeJSON(tasksPath, data);
|
||||||
log("success", "Fixed dependency issues in tasks.json");
|
log('success', 'Fixed dependency issues in tasks.json');
|
||||||
|
|
||||||
// Regenerate task files
|
// Regenerate task files
|
||||||
log("info", "Regenerating task files to reflect dependency changes...");
|
log('info', 'Regenerating task files to reflect dependency changes...');
|
||||||
await generateTaskFiles(tasksPath, path.dirname(tasksPath));
|
await generateTaskFiles(tasksPath, path.dirname(tasksPath));
|
||||||
} else {
|
} else {
|
||||||
log("info", "No changes needed to fix dependencies");
|
log('info', 'No changes needed to fix dependencies');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show detailed statistics report
|
// Show detailed statistics report
|
||||||
@@ -1023,48 +1023,48 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
|
|||||||
|
|
||||||
if (!isSilentMode()) {
|
if (!isSilentMode()) {
|
||||||
if (totalFixedAll > 0) {
|
if (totalFixedAll > 0) {
|
||||||
log("success", `Fixed ${totalFixedAll} dependency issues in total!`);
|
log('success', `Fixed ${totalFixedAll} dependency issues in total!`);
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
boxen(
|
boxen(
|
||||||
chalk.green(`Dependency Fixes Summary:\n\n`) +
|
chalk.green(`Dependency Fixes Summary:\n\n`) +
|
||||||
`${chalk.cyan("Invalid dependencies removed:")} ${stats.nonExistentDependenciesRemoved}\n` +
|
`${chalk.cyan('Invalid dependencies removed:')} ${stats.nonExistentDependenciesRemoved}\n` +
|
||||||
`${chalk.cyan("Self-dependencies removed:")} ${stats.selfDependenciesRemoved}\n` +
|
`${chalk.cyan('Self-dependencies removed:')} ${stats.selfDependenciesRemoved}\n` +
|
||||||
`${chalk.cyan("Duplicate dependencies removed:")} ${stats.duplicateDependenciesRemoved}\n` +
|
`${chalk.cyan('Duplicate dependencies removed:')} ${stats.duplicateDependenciesRemoved}\n` +
|
||||||
`${chalk.cyan("Circular dependencies fixed:")} ${stats.circularDependenciesFixed}\n\n` +
|
`${chalk.cyan('Circular dependencies fixed:')} ${stats.circularDependenciesFixed}\n\n` +
|
||||||
`${chalk.cyan("Tasks fixed:")} ${stats.tasksFixed}\n` +
|
`${chalk.cyan('Tasks fixed:')} ${stats.tasksFixed}\n` +
|
||||||
`${chalk.cyan("Subtasks fixed:")} ${stats.subtasksFixed}\n`,
|
`${chalk.cyan('Subtasks fixed:')} ${stats.subtasksFixed}\n`,
|
||||||
{
|
{
|
||||||
padding: 1,
|
padding: 1,
|
||||||
borderColor: "green",
|
borderColor: 'green',
|
||||||
borderStyle: "round",
|
borderStyle: 'round',
|
||||||
margin: { top: 1, bottom: 1 },
|
margin: { top: 1, bottom: 1 }
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
log(
|
log(
|
||||||
"success",
|
'success',
|
||||||
"No dependency issues found - all dependencies are valid"
|
'No dependency issues found - all dependencies are valid'
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
boxen(
|
boxen(
|
||||||
chalk.green(`All Dependencies Are Valid\n\n`) +
|
chalk.green(`All Dependencies Are Valid\n\n`) +
|
||||||
`${chalk.cyan("Tasks checked:")} ${data.tasks.length}\n` +
|
`${chalk.cyan('Tasks checked:')} ${data.tasks.length}\n` +
|
||||||
`${chalk.cyan("Total dependencies verified:")} ${countAllDependencies(data.tasks)}`,
|
`${chalk.cyan('Total dependencies verified:')} ${countAllDependencies(data.tasks)}`,
|
||||||
{
|
{
|
||||||
padding: 1,
|
padding: 1,
|
||||||
borderColor: "green",
|
borderColor: 'green',
|
||||||
borderStyle: "round",
|
borderStyle: 'round',
|
||||||
margin: { top: 1, bottom: 1 },
|
margin: { top: 1, bottom: 1 }
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log("error", "Error in fix-dependencies command:", error);
|
log('error', 'Error in fix-dependencies command:', error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1103,7 +1103,7 @@ function ensureAtLeastOneIndependentSubtask(tasksData) {
|
|||||||
if (task.subtasks.length > 0) {
|
if (task.subtasks.length > 0) {
|
||||||
const firstSubtask = task.subtasks[0];
|
const firstSubtask = task.subtasks[0];
|
||||||
log(
|
log(
|
||||||
"debug",
|
'debug',
|
||||||
`Ensuring at least one independent subtask: Clearing dependencies for subtask ${task.id}.${firstSubtask.id}`
|
`Ensuring at least one independent subtask: Clearing dependencies for subtask ${task.id}.${firstSubtask.id}`
|
||||||
);
|
);
|
||||||
firstSubtask.dependencies = [];
|
firstSubtask.dependencies = [];
|
||||||
@@ -1124,11 +1124,11 @@ function ensureAtLeastOneIndependentSubtask(tasksData) {
|
|||||||
*/
|
*/
|
||||||
function validateAndFixDependencies(tasksData, tasksPath = null) {
|
function validateAndFixDependencies(tasksData, tasksPath = null) {
|
||||||
if (!tasksData || !tasksData.tasks || !Array.isArray(tasksData.tasks)) {
|
if (!tasksData || !tasksData.tasks || !Array.isArray(tasksData.tasks)) {
|
||||||
log("error", "Invalid tasks data");
|
log('error', 'Invalid tasks data');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
log("debug", "Validating and fixing dependencies...");
|
log('debug', 'Validating and fixing dependencies...');
|
||||||
|
|
||||||
// Create a deep copy for comparison
|
// Create a deep copy for comparison
|
||||||
const originalData = JSON.parse(JSON.stringify(tasksData));
|
const originalData = JSON.parse(JSON.stringify(tasksData));
|
||||||
@@ -1174,7 +1174,7 @@ function validateAndFixDependencies(tasksData, tasksPath = null) {
|
|||||||
if (subtask.dependencies) {
|
if (subtask.dependencies) {
|
||||||
subtask.dependencies = subtask.dependencies.filter((depId) => {
|
subtask.dependencies = subtask.dependencies.filter((depId) => {
|
||||||
// Handle numeric subtask references
|
// Handle numeric subtask references
|
||||||
if (typeof depId === "number" && depId < 100) {
|
if (typeof depId === 'number' && depId < 100) {
|
||||||
const fullSubtaskId = `${task.id}.${depId}`;
|
const fullSubtaskId = `${task.id}.${depId}`;
|
||||||
return taskExists(tasksData.tasks, fullSubtaskId);
|
return taskExists(tasksData.tasks, fullSubtaskId);
|
||||||
}
|
}
|
||||||
@@ -1210,9 +1210,9 @@ function validateAndFixDependencies(tasksData, tasksPath = null) {
|
|||||||
if (tasksPath && changesDetected) {
|
if (tasksPath && changesDetected) {
|
||||||
try {
|
try {
|
||||||
writeJSON(tasksPath, tasksData);
|
writeJSON(tasksPath, tasksData);
|
||||||
log("debug", "Saved dependency fixes to tasks.json");
|
log('debug', 'Saved dependency fixes to tasks.json');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log("error", "Failed to save dependency fixes to tasks.json", error);
|
log('error', 'Failed to save dependency fixes to tasks.json', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1229,5 +1229,5 @@ export {
|
|||||||
removeDuplicateDependencies,
|
removeDuplicateDependencies,
|
||||||
cleanupSubtaskDependencies,
|
cleanupSubtaskDependencies,
|
||||||
ensureAtLeastOneIndependentSubtask,
|
ensureAtLeastOneIndependentSubtask,
|
||||||
validateAndFixDependencies,
|
validateAndFixDependencies
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import path from "path";
|
import path from 'path';
|
||||||
import chalk from "chalk";
|
import chalk from 'chalk';
|
||||||
import boxen from "boxen";
|
import boxen from 'boxen';
|
||||||
import Table from "cli-table3";
|
import Table from 'cli-table3';
|
||||||
import { z } from "zod";
|
import { z } from 'zod';
|
||||||
import Fuse from "fuse.js"; // Import Fuse.js for advanced fuzzy search
|
import Fuse from 'fuse.js'; // Import Fuse.js for advanced fuzzy search
|
||||||
|
|
||||||
import {
|
import {
|
||||||
displayBanner,
|
displayBanner,
|
||||||
@@ -12,31 +12,31 @@ import {
|
|||||||
stopLoadingIndicator,
|
stopLoadingIndicator,
|
||||||
succeedLoadingIndicator,
|
succeedLoadingIndicator,
|
||||||
failLoadingIndicator,
|
failLoadingIndicator,
|
||||||
displayAiUsageSummary,
|
displayAiUsageSummary
|
||||||
} from "../ui.js";
|
} from '../ui.js';
|
||||||
import { readJSON, writeJSON, log as consoleLog, truncate } from "../utils.js";
|
import { readJSON, writeJSON, log as consoleLog, truncate } from '../utils.js';
|
||||||
import { generateObjectService } from "../ai-services-unified.js";
|
import { generateObjectService } from '../ai-services-unified.js';
|
||||||
import { getDefaultPriority } from "../config-manager.js";
|
import { getDefaultPriority } from '../config-manager.js';
|
||||||
import generateTaskFiles from "./generate-task-files.js";
|
import generateTaskFiles from './generate-task-files.js';
|
||||||
|
|
||||||
// Define Zod schema for the expected AI output object
|
// Define Zod schema for the expected AI output object
|
||||||
const AiTaskDataSchema = z.object({
|
const AiTaskDataSchema = z.object({
|
||||||
title: z.string().describe("Clear, concise title for the task"),
|
title: z.string().describe('Clear, concise title for the task'),
|
||||||
description: z
|
description: z
|
||||||
.string()
|
.string()
|
||||||
.describe("A one or two sentence description of the task"),
|
.describe('A one or two sentence description of the task'),
|
||||||
details: z
|
details: z
|
||||||
.string()
|
.string()
|
||||||
.describe("In-depth implementation details, considerations, and guidance"),
|
.describe('In-depth implementation details, considerations, and guidance'),
|
||||||
testStrategy: z
|
testStrategy: z
|
||||||
.string()
|
.string()
|
||||||
.describe("Detailed approach for verifying task completion"),
|
.describe('Detailed approach for verifying task completion'),
|
||||||
dependencies: z
|
dependencies: z
|
||||||
.array(z.number())
|
.array(z.number())
|
||||||
.optional()
|
.optional()
|
||||||
.describe(
|
.describe(
|
||||||
"Array of task IDs that this task depends on (must be completed before this task can start)"
|
'Array of task IDs that this task depends on (must be completed before this task can start)'
|
||||||
),
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -64,7 +64,7 @@ async function addTask(
|
|||||||
dependencies = [],
|
dependencies = [],
|
||||||
priority = null,
|
priority = null,
|
||||||
context = {},
|
context = {},
|
||||||
outputFormat = "text", // Default to text for CLI
|
outputFormat = 'text', // Default to text for CLI
|
||||||
manualTaskData = null,
|
manualTaskData = null,
|
||||||
useResearch = false
|
useResearch = false
|
||||||
) {
|
) {
|
||||||
@@ -76,27 +76,27 @@ async function addTask(
|
|||||||
? mcpLog // Use MCP logger if provided
|
? mcpLog // Use MCP logger if provided
|
||||||
: {
|
: {
|
||||||
// Create a wrapper around consoleLog for CLI
|
// Create a wrapper around consoleLog for CLI
|
||||||
info: (...args) => consoleLog("info", ...args),
|
info: (...args) => consoleLog('info', ...args),
|
||||||
warn: (...args) => consoleLog("warn", ...args),
|
warn: (...args) => consoleLog('warn', ...args),
|
||||||
error: (...args) => consoleLog("error", ...args),
|
error: (...args) => consoleLog('error', ...args),
|
||||||
debug: (...args) => consoleLog("debug", ...args),
|
debug: (...args) => consoleLog('debug', ...args),
|
||||||
success: (...args) => consoleLog("success", ...args),
|
success: (...args) => consoleLog('success', ...args)
|
||||||
};
|
};
|
||||||
|
|
||||||
const effectivePriority = priority || getDefaultPriority(projectRoot);
|
const effectivePriority = priority || getDefaultPriority(projectRoot);
|
||||||
|
|
||||||
logFn.info(
|
logFn.info(
|
||||||
`Adding new task with prompt: "${prompt}", Priority: ${effectivePriority}, Dependencies: ${dependencies.join(", ") || "None"}, Research: ${useResearch}, ProjectRoot: ${projectRoot}`
|
`Adding new task with prompt: "${prompt}", Priority: ${effectivePriority}, Dependencies: ${dependencies.join(', ') || 'None'}, Research: ${useResearch}, ProjectRoot: ${projectRoot}`
|
||||||
);
|
);
|
||||||
|
|
||||||
let loadingIndicator = null;
|
let loadingIndicator = null;
|
||||||
let aiServiceResponse = null; // To store the full response from AI service
|
let aiServiceResponse = null; // To store the full response from AI service
|
||||||
|
|
||||||
// Create custom reporter that checks for MCP log
|
// Create custom reporter that checks for MCP log
|
||||||
const report = (message, level = "info") => {
|
const report = (message, level = 'info') => {
|
||||||
if (mcpLog) {
|
if (mcpLog) {
|
||||||
mcpLog[level](message);
|
mcpLog[level](message);
|
||||||
} else if (outputFormat === "text") {
|
} else if (outputFormat === 'text') {
|
||||||
consoleLog(level, message);
|
consoleLog(level, message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -158,7 +158,7 @@ async function addTask(
|
|||||||
title: task.title,
|
title: task.title,
|
||||||
description: task.description,
|
description: task.description,
|
||||||
status: task.status,
|
status: task.status,
|
||||||
dependencies: dependencyData,
|
dependencies: dependencyData
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,14 +168,14 @@ async function addTask(
|
|||||||
|
|
||||||
// If tasks.json doesn't exist or is invalid, create a new one
|
// If tasks.json doesn't exist or is invalid, create a new one
|
||||||
if (!data || !data.tasks) {
|
if (!data || !data.tasks) {
|
||||||
report("tasks.json not found or invalid. Creating a new one.", "info");
|
report('tasks.json not found or invalid. Creating a new one.', 'info');
|
||||||
// Create default tasks data structure
|
// Create default tasks data structure
|
||||||
data = {
|
data = {
|
||||||
tasks: [],
|
tasks: []
|
||||||
};
|
};
|
||||||
// Ensure the directory exists and write the new file
|
// Ensure the directory exists and write the new file
|
||||||
writeJSON(tasksPath, data);
|
writeJSON(tasksPath, data);
|
||||||
report("Created new tasks.json file with empty tasks array.", "info");
|
report('Created new tasks.json file with empty tasks array.', 'info');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the highest task ID to determine the next ID
|
// Find the highest task ID to determine the next ID
|
||||||
@@ -184,13 +184,13 @@ async function addTask(
|
|||||||
const newTaskId = highestId + 1;
|
const newTaskId = highestId + 1;
|
||||||
|
|
||||||
// Only show UI box for CLI mode
|
// Only show UI box for CLI mode
|
||||||
if (outputFormat === "text") {
|
if (outputFormat === 'text') {
|
||||||
console.log(
|
console.log(
|
||||||
boxen(chalk.white.bold(`Creating New Task #${newTaskId}`), {
|
boxen(chalk.white.bold(`Creating New Task #${newTaskId}`), {
|
||||||
padding: 1,
|
padding: 1,
|
||||||
borderColor: "blue",
|
borderColor: 'blue',
|
||||||
borderStyle: "round",
|
borderStyle: 'round',
|
||||||
margin: { top: 1, bottom: 1 },
|
margin: { top: 1, bottom: 1 }
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -204,10 +204,10 @@ async function addTask(
|
|||||||
|
|
||||||
if (invalidDeps.length > 0) {
|
if (invalidDeps.length > 0) {
|
||||||
report(
|
report(
|
||||||
`The following dependencies do not exist or are invalid: ${invalidDeps.join(", ")}`,
|
`The following dependencies do not exist or are invalid: ${invalidDeps.join(', ')}`,
|
||||||
"warn"
|
'warn'
|
||||||
);
|
);
|
||||||
report("Removing invalid dependencies...", "info");
|
report('Removing invalid dependencies...', 'info');
|
||||||
dependencies = dependencies.filter(
|
dependencies = dependencies.filter(
|
||||||
(depId) => !invalidDeps.includes(depId)
|
(depId) => !invalidDeps.includes(depId)
|
||||||
);
|
);
|
||||||
@@ -242,28 +242,28 @@ async function addTask(
|
|||||||
|
|
||||||
// Check if manual task data is provided
|
// Check if manual task data is provided
|
||||||
if (manualTaskData) {
|
if (manualTaskData) {
|
||||||
report("Using manually provided task data", "info");
|
report('Using manually provided task data', 'info');
|
||||||
taskData = manualTaskData;
|
taskData = manualTaskData;
|
||||||
report("DEBUG: Taking MANUAL task data path.", "debug");
|
report('DEBUG: Taking MANUAL task data path.', 'debug');
|
||||||
|
|
||||||
// Basic validation for manual data
|
// Basic validation for manual data
|
||||||
if (
|
if (
|
||||||
!taskData.title ||
|
!taskData.title ||
|
||||||
typeof taskData.title !== "string" ||
|
typeof taskData.title !== 'string' ||
|
||||||
!taskData.description ||
|
!taskData.description ||
|
||||||
typeof taskData.description !== "string"
|
typeof taskData.description !== 'string'
|
||||||
) {
|
) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Manual task data must include at least a title and description."
|
'Manual task data must include at least a title and description.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
report("DEBUG: Taking AI task generation path.", "debug");
|
report('DEBUG: Taking AI task generation path.', 'debug');
|
||||||
// --- Refactored AI Interaction ---
|
// --- Refactored AI Interaction ---
|
||||||
report(`Generating task data with AI with prompt:\n${prompt}`, "info");
|
report(`Generating task data with AI with prompt:\n${prompt}`, 'info');
|
||||||
|
|
||||||
// Create context string for task creation prompt
|
// Create context string for task creation prompt
|
||||||
let contextTasks = "";
|
let contextTasks = '';
|
||||||
|
|
||||||
// Create a dependency map for better understanding of the task relationships
|
// Create a dependency map for better understanding of the task relationships
|
||||||
const taskMap = {};
|
const taskMap = {};
|
||||||
@@ -274,18 +274,18 @@ async function addTask(
|
|||||||
title: t.title,
|
title: t.title,
|
||||||
description: t.description,
|
description: t.description,
|
||||||
dependencies: t.dependencies || [],
|
dependencies: t.dependencies || [],
|
||||||
status: t.status,
|
status: t.status
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// CLI-only feedback for the dependency analysis
|
// CLI-only feedback for the dependency analysis
|
||||||
if (outputFormat === "text") {
|
if (outputFormat === 'text') {
|
||||||
console.log(
|
console.log(
|
||||||
boxen(chalk.cyan.bold("Task Context Analysis"), {
|
boxen(chalk.cyan.bold('Task Context Analysis'), {
|
||||||
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
||||||
margin: { top: 0, bottom: 0 },
|
margin: { top: 0, bottom: 0 },
|
||||||
borderColor: "cyan",
|
borderColor: 'cyan',
|
||||||
borderStyle: "round",
|
borderStyle: 'round'
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -316,7 +316,7 @@ async function addTask(
|
|||||||
const directDeps = data.tasks.filter((t) =>
|
const directDeps = data.tasks.filter((t) =>
|
||||||
numericDependencies.includes(t.id)
|
numericDependencies.includes(t.id)
|
||||||
);
|
);
|
||||||
contextTasks += `\n${directDeps.map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`).join("\n")}`;
|
contextTasks += `\n${directDeps.map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`).join('\n')}`;
|
||||||
|
|
||||||
// Add an overview of indirect dependencies if present
|
// Add an overview of indirect dependencies if present
|
||||||
const indirectDeps = dependentTasks.filter(
|
const indirectDeps = dependentTasks.filter(
|
||||||
@@ -327,7 +327,7 @@ async function addTask(
|
|||||||
contextTasks += `\n${indirectDeps
|
contextTasks += `\n${indirectDeps
|
||||||
.slice(0, 5)
|
.slice(0, 5)
|
||||||
.map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`)
|
.map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`)
|
||||||
.join("\n")}`;
|
.join('\n')}`;
|
||||||
if (indirectDeps.length > 5) {
|
if (indirectDeps.length > 5) {
|
||||||
contextTasks += `\n- ... and ${indirectDeps.length - 5} more indirect dependencies`;
|
contextTasks += `\n- ... and ${indirectDeps.length - 5} more indirect dependencies`;
|
||||||
}
|
}
|
||||||
@@ -338,15 +338,15 @@ async function addTask(
|
|||||||
for (const depTask of uniqueDetailedTasks) {
|
for (const depTask of uniqueDetailedTasks) {
|
||||||
const depthInfo = depthMap.get(depTask.id)
|
const depthInfo = depthMap.get(depTask.id)
|
||||||
? ` (depth: ${depthMap.get(depTask.id)})`
|
? ` (depth: ${depthMap.get(depTask.id)})`
|
||||||
: "";
|
: '';
|
||||||
const isDirect = numericDependencies.includes(depTask.id)
|
const isDirect = numericDependencies.includes(depTask.id)
|
||||||
? " [DIRECT DEPENDENCY]"
|
? ' [DIRECT DEPENDENCY]'
|
||||||
: "";
|
: '';
|
||||||
|
|
||||||
contextTasks += `\n\n------ Task ${depTask.id}${isDirect}${depthInfo}: ${depTask.title} ------\n`;
|
contextTasks += `\n\n------ Task ${depTask.id}${isDirect}${depthInfo}: ${depTask.title} ------\n`;
|
||||||
contextTasks += `Description: ${depTask.description}\n`;
|
contextTasks += `Description: ${depTask.description}\n`;
|
||||||
contextTasks += `Status: ${depTask.status || "pending"}\n`;
|
contextTasks += `Status: ${depTask.status || 'pending'}\n`;
|
||||||
contextTasks += `Priority: ${depTask.priority || "medium"}\n`;
|
contextTasks += `Priority: ${depTask.priority || 'medium'}\n`;
|
||||||
|
|
||||||
// List its dependencies
|
// List its dependencies
|
||||||
if (depTask.dependencies && depTask.dependencies.length > 0) {
|
if (depTask.dependencies && depTask.dependencies.length > 0) {
|
||||||
@@ -356,7 +356,7 @@ async function addTask(
|
|||||||
? `Task ${dId}: ${depDepTask.title}`
|
? `Task ${dId}: ${depDepTask.title}`
|
||||||
: `Task ${dId}`;
|
: `Task ${dId}`;
|
||||||
});
|
});
|
||||||
contextTasks += `Dependencies: ${depDeps.join(", ")}\n`;
|
contextTasks += `Dependencies: ${depDeps.join(', ')}\n`;
|
||||||
} else {
|
} else {
|
||||||
contextTasks += `Dependencies: None\n`;
|
contextTasks += `Dependencies: None\n`;
|
||||||
}
|
}
|
||||||
@@ -365,7 +365,7 @@ async function addTask(
|
|||||||
if (depTask.details) {
|
if (depTask.details) {
|
||||||
const truncatedDetails =
|
const truncatedDetails =
|
||||||
depTask.details.length > 400
|
depTask.details.length > 400
|
||||||
? depTask.details.substring(0, 400) + "... (truncated)"
|
? depTask.details.substring(0, 400) + '... (truncated)'
|
||||||
: depTask.details;
|
: depTask.details;
|
||||||
contextTasks += `Implementation Details: ${truncatedDetails}\n`;
|
contextTasks += `Implementation Details: ${truncatedDetails}\n`;
|
||||||
}
|
}
|
||||||
@@ -373,19 +373,19 @@ async function addTask(
|
|||||||
|
|
||||||
// Add dependency chain visualization
|
// Add dependency chain visualization
|
||||||
if (dependencyGraphs.length > 0) {
|
if (dependencyGraphs.length > 0) {
|
||||||
contextTasks += "\n\nDependency Chain Visualization:";
|
contextTasks += '\n\nDependency Chain Visualization:';
|
||||||
|
|
||||||
// Helper function to format dependency chain as text
|
// Helper function to format dependency chain as text
|
||||||
function formatDependencyChain(
|
function formatDependencyChain(
|
||||||
node,
|
node,
|
||||||
prefix = "",
|
prefix = '',
|
||||||
isLast = true,
|
isLast = true,
|
||||||
depth = 0
|
depth = 0
|
||||||
) {
|
) {
|
||||||
if (depth > 3) return ""; // Limit depth to avoid excessive nesting
|
if (depth > 3) return ''; // Limit depth to avoid excessive nesting
|
||||||
|
|
||||||
const connector = isLast ? "└── " : "├── ";
|
const connector = isLast ? '└── ' : '├── ';
|
||||||
const childPrefix = isLast ? " " : "│ ";
|
const childPrefix = isLast ? ' ' : '│ ';
|
||||||
|
|
||||||
let result = `\n${prefix}${connector}Task ${node.id}: ${node.title}`;
|
let result = `\n${prefix}${connector}Task ${node.id}: ${node.title}`;
|
||||||
|
|
||||||
@@ -411,7 +411,7 @@ async function addTask(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Show dependency analysis in CLI mode
|
// Show dependency analysis in CLI mode
|
||||||
if (outputFormat === "text") {
|
if (outputFormat === 'text') {
|
||||||
if (directDeps.length > 0) {
|
if (directDeps.length > 0) {
|
||||||
console.log(chalk.gray(` Explicitly specified dependencies:`));
|
console.log(chalk.gray(` Explicitly specified dependencies:`));
|
||||||
directDeps.forEach((t) => {
|
directDeps.forEach((t) => {
|
||||||
@@ -451,14 +451,14 @@ async function addTask(
|
|||||||
// Convert dependency graph to ASCII art for terminal
|
// Convert dependency graph to ASCII art for terminal
|
||||||
function visualizeDependencyGraph(
|
function visualizeDependencyGraph(
|
||||||
node,
|
node,
|
||||||
prefix = "",
|
prefix = '',
|
||||||
isLast = true,
|
isLast = true,
|
||||||
depth = 0
|
depth = 0
|
||||||
) {
|
) {
|
||||||
if (depth > 2) return; // Limit depth for display
|
if (depth > 2) return; // Limit depth for display
|
||||||
|
|
||||||
const connector = isLast ? "└── " : "├── ";
|
const connector = isLast ? '└── ' : '├── ';
|
||||||
const childPrefix = isLast ? " " : "│ ";
|
const childPrefix = isLast ? ' ' : '│ ';
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
chalk.blue(
|
chalk.blue(
|
||||||
@@ -494,18 +494,18 @@ async function addTask(
|
|||||||
includeScore: true, // Return match scores
|
includeScore: true, // Return match scores
|
||||||
threshold: 0.4, // Lower threshold = stricter matching (range 0-1)
|
threshold: 0.4, // Lower threshold = stricter matching (range 0-1)
|
||||||
keys: [
|
keys: [
|
||||||
{ name: "title", weight: 1.5 }, // Title is most important
|
{ name: 'title', weight: 1.5 }, // Title is most important
|
||||||
{ name: "description", weight: 2 }, // Description is very important
|
{ name: 'description', weight: 2 }, // Description is very important
|
||||||
{ name: "details", weight: 3 }, // Details is most important
|
{ name: 'details', weight: 3 }, // Details is most important
|
||||||
// Search dependencies to find tasks that depend on similar things
|
// Search dependencies to find tasks that depend on similar things
|
||||||
{ name: "dependencyTitles", weight: 0.5 },
|
{ name: 'dependencyTitles', weight: 0.5 }
|
||||||
],
|
],
|
||||||
// Sort matches by score (lower is better)
|
// Sort matches by score (lower is better)
|
||||||
shouldSort: true,
|
shouldSort: true,
|
||||||
// Allow searching in nested properties
|
// Allow searching in nested properties
|
||||||
useExtendedSearch: true,
|
useExtendedSearch: true,
|
||||||
// Return up to 50 matches
|
// Return up to 50 matches
|
||||||
limit: 50,
|
limit: 50
|
||||||
};
|
};
|
||||||
|
|
||||||
// Prepare task data with dependencies expanded as titles for better semantic search
|
// Prepare task data with dependencies expanded as titles for better semantic search
|
||||||
@@ -516,15 +516,15 @@ async function addTask(
|
|||||||
? task.dependencies
|
? task.dependencies
|
||||||
.map((depId) => {
|
.map((depId) => {
|
||||||
const depTask = data.tasks.find((t) => t.id === depId);
|
const depTask = data.tasks.find((t) => t.id === depId);
|
||||||
return depTask ? depTask.title : "";
|
return depTask ? depTask.title : '';
|
||||||
})
|
})
|
||||||
.filter((title) => title)
|
.filter((title) => title)
|
||||||
.join(" ")
|
.join(' ')
|
||||||
: "";
|
: '';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...task,
|
...task,
|
||||||
dependencyTitles,
|
dependencyTitles
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -534,7 +534,7 @@ async function addTask(
|
|||||||
// Extract significant words and phrases from the prompt
|
// Extract significant words and phrases from the prompt
|
||||||
const promptWords = prompt
|
const promptWords = prompt
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/[^\w\s-]/g, " ") // Replace non-alphanumeric chars with spaces
|
.replace(/[^\w\s-]/g, ' ') // Replace non-alphanumeric chars with spaces
|
||||||
.split(/\s+/)
|
.split(/\s+/)
|
||||||
.filter((word) => word.length > 3); // Words at least 4 chars
|
.filter((word) => word.length > 3); // Words at least 4 chars
|
||||||
|
|
||||||
@@ -602,21 +602,21 @@ async function addTask(
|
|||||||
if (relatedTasks.length > 0) {
|
if (relatedTasks.length > 0) {
|
||||||
contextTasks = `\nRelevant tasks identified by semantic similarity:\n${relatedTasks
|
contextTasks = `\nRelevant tasks identified by semantic similarity:\n${relatedTasks
|
||||||
.map((t, i) => {
|
.map((t, i) => {
|
||||||
const relevanceMarker = i < highRelevance.length ? "⭐ " : "";
|
const relevanceMarker = i < highRelevance.length ? '⭐ ' : '';
|
||||||
return `- ${relevanceMarker}Task ${t.id}: ${t.title} - ${t.description}`;
|
return `- ${relevanceMarker}Task ${t.id}: ${t.title} - ${t.description}`;
|
||||||
})
|
})
|
||||||
.join("\n")}`;
|
.join('\n')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
recentTasks.length > 0 &&
|
recentTasks.length > 0 &&
|
||||||
!contextTasks.includes("Recently created tasks")
|
!contextTasks.includes('Recently created tasks')
|
||||||
) {
|
) {
|
||||||
contextTasks += `\n\nRecently created tasks:\n${recentTasks
|
contextTasks += `\n\nRecently created tasks:\n${recentTasks
|
||||||
.filter((t) => !relatedTasks.some((rt) => rt.id === t.id))
|
.filter((t) => !relatedTasks.some((rt) => rt.id === t.id))
|
||||||
.slice(0, 3)
|
.slice(0, 3)
|
||||||
.map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`)
|
.map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`)
|
||||||
.join("\n")}`;
|
.join('\n')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add detailed information about the most relevant tasks
|
// Add detailed information about the most relevant tasks
|
||||||
@@ -630,8 +630,8 @@ async function addTask(
|
|||||||
for (const task of uniqueDetailedTasks) {
|
for (const task of uniqueDetailedTasks) {
|
||||||
contextTasks += `\n\n------ Task ${task.id}: ${task.title} ------\n`;
|
contextTasks += `\n\n------ Task ${task.id}: ${task.title} ------\n`;
|
||||||
contextTasks += `Description: ${task.description}\n`;
|
contextTasks += `Description: ${task.description}\n`;
|
||||||
contextTasks += `Status: ${task.status || "pending"}\n`;
|
contextTasks += `Status: ${task.status || 'pending'}\n`;
|
||||||
contextTasks += `Priority: ${task.priority || "medium"}\n`;
|
contextTasks += `Priority: ${task.priority || 'medium'}\n`;
|
||||||
if (task.dependencies && task.dependencies.length > 0) {
|
if (task.dependencies && task.dependencies.length > 0) {
|
||||||
// Format dependency list with titles
|
// Format dependency list with titles
|
||||||
const depList = task.dependencies.map((depId) => {
|
const depList = task.dependencies.map((depId) => {
|
||||||
@@ -640,13 +640,13 @@ async function addTask(
|
|||||||
? `Task ${depId} (${depTask.title})`
|
? `Task ${depId} (${depTask.title})`
|
||||||
: `Task ${depId}`;
|
: `Task ${depId}`;
|
||||||
});
|
});
|
||||||
contextTasks += `Dependencies: ${depList.join(", ")}\n`;
|
contextTasks += `Dependencies: ${depList.join(', ')}\n`;
|
||||||
}
|
}
|
||||||
// Add implementation details but truncate if too long
|
// Add implementation details but truncate if too long
|
||||||
if (task.details) {
|
if (task.details) {
|
||||||
const truncatedDetails =
|
const truncatedDetails =
|
||||||
task.details.length > 400
|
task.details.length > 400
|
||||||
? task.details.substring(0, 400) + "... (truncated)"
|
? task.details.substring(0, 400) + '... (truncated)'
|
||||||
: task.details;
|
: task.details;
|
||||||
contextTasks += `Implementation Details: ${truncatedDetails}\n`;
|
contextTasks += `Implementation Details: ${truncatedDetails}\n`;
|
||||||
}
|
}
|
||||||
@@ -654,7 +654,7 @@ async function addTask(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add a concise view of the task dependency structure
|
// Add a concise view of the task dependency structure
|
||||||
contextTasks += "\n\nSummary of task dependencies in the project:";
|
contextTasks += '\n\nSummary of task dependencies in the project:';
|
||||||
|
|
||||||
// Get pending/in-progress tasks that might be most relevant based on fuzzy search
|
// Get pending/in-progress tasks that might be most relevant based on fuzzy search
|
||||||
// Prioritize tasks from our similarity search
|
// Prioritize tasks from our similarity search
|
||||||
@@ -662,7 +662,7 @@ async function addTask(
|
|||||||
const relevantPendingTasks = data.tasks
|
const relevantPendingTasks = data.tasks
|
||||||
.filter(
|
.filter(
|
||||||
(t) =>
|
(t) =>
|
||||||
(t.status === "pending" || t.status === "in-progress") &&
|
(t.status === 'pending' || t.status === 'in-progress') &&
|
||||||
// Either in our relevant set OR has relevant words in title/description
|
// Either in our relevant set OR has relevant words in title/description
|
||||||
(relevantTaskIds.has(t.id) ||
|
(relevantTaskIds.has(t.id) ||
|
||||||
promptWords.some(
|
promptWords.some(
|
||||||
@@ -676,8 +676,8 @@ async function addTask(
|
|||||||
for (const task of relevantPendingTasks) {
|
for (const task of relevantPendingTasks) {
|
||||||
const depsStr =
|
const depsStr =
|
||||||
task.dependencies && task.dependencies.length > 0
|
task.dependencies && task.dependencies.length > 0
|
||||||
? task.dependencies.join(", ")
|
? task.dependencies.join(', ')
|
||||||
: "None";
|
: 'None';
|
||||||
contextTasks += `\n- Task ${task.id}: depends on [${depsStr}]`;
|
contextTasks += `\n- Task ${task.id}: depends on [${depsStr}]`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -709,7 +709,7 @@ async function addTask(
|
|||||||
.slice(0, 10);
|
.slice(0, 10);
|
||||||
|
|
||||||
if (commonDeps.length > 0) {
|
if (commonDeps.length > 0) {
|
||||||
contextTasks += "\nMost common dependencies for similar tasks:";
|
contextTasks += '\nMost common dependencies for similar tasks:';
|
||||||
commonDeps.forEach(([depId, count]) => {
|
commonDeps.forEach(([depId, count]) => {
|
||||||
const depTask = data.tasks.find((t) => t.id === parseInt(depId));
|
const depTask = data.tasks.find((t) => t.id === parseInt(depId));
|
||||||
if (depTask) {
|
if (depTask) {
|
||||||
@@ -720,7 +720,7 @@ async function addTask(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Show fuzzy search analysis in CLI mode
|
// Show fuzzy search analysis in CLI mode
|
||||||
if (outputFormat === "text") {
|
if (outputFormat === 'text') {
|
||||||
console.log(
|
console.log(
|
||||||
chalk.gray(
|
chalk.gray(
|
||||||
` Context search across ${data.tasks.length} tasks using full prompt and ${promptWords.length} keywords`
|
` Context search across ${data.tasks.length} tasks using full prompt and ${promptWords.length} keywords`
|
||||||
@@ -777,7 +777,7 @@ async function addTask(
|
|||||||
const isHighRelevance = highRelevance.some(
|
const isHighRelevance = highRelevance.some(
|
||||||
(ht) => ht.id === t.id
|
(ht) => ht.id === t.id
|
||||||
);
|
);
|
||||||
const relevanceIndicator = isHighRelevance ? "⭐ " : "";
|
const relevanceIndicator = isHighRelevance ? '⭐ ' : '';
|
||||||
console.log(
|
console.log(
|
||||||
chalk.cyan(
|
chalk.cyan(
|
||||||
` • ${relevanceIndicator}Task ${t.id}: ${truncate(t.title, 40)}`
|
` • ${relevanceIndicator}Task ${t.id}: ${truncate(t.title, 40)}`
|
||||||
@@ -805,14 +805,14 @@ async function addTask(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add a visual transition to show we're moving to AI generation - only for CLI
|
// Add a visual transition to show we're moving to AI generation - only for CLI
|
||||||
if (outputFormat === "text") {
|
if (outputFormat === 'text') {
|
||||||
console.log(
|
console.log(
|
||||||
boxen(
|
boxen(
|
||||||
chalk.white.bold("AI Task Generation") +
|
chalk.white.bold('AI Task Generation') +
|
||||||
`\n\n${chalk.gray("Analyzing context and generating task details using AI...")}` +
|
`\n\n${chalk.gray('Analyzing context and generating task details using AI...')}` +
|
||||||
`\n${chalk.cyan("Context size: ")}${chalk.yellow(contextTasks.length.toLocaleString())} characters` +
|
`\n${chalk.cyan('Context size: ')}${chalk.yellow(contextTasks.length.toLocaleString())} characters` +
|
||||||
`\n${chalk.cyan("Dependency detection: ")}${chalk.yellow(numericDependencies.length > 0 ? "Explicit dependencies" : "Auto-discovery mode")}` +
|
`\n${chalk.cyan('Dependency detection: ')}${chalk.yellow(numericDependencies.length > 0 ? 'Explicit dependencies' : 'Auto-discovery mode')}` +
|
||||||
`\n${chalk.cyan("Detailed tasks: ")}${chalk.yellow(
|
`\n${chalk.cyan('Detailed tasks: ')}${chalk.yellow(
|
||||||
numericDependencies.length > 0
|
numericDependencies.length > 0
|
||||||
? dependentTasks.length // Use length of tasks from explicit dependency path
|
? dependentTasks.length // Use length of tasks from explicit dependency path
|
||||||
: uniqueDetailedTasks.length // Use length of tasks from fuzzy search path
|
: uniqueDetailedTasks.length // Use length of tasks from fuzzy search path
|
||||||
@@ -820,8 +820,8 @@ async function addTask(
|
|||||||
{
|
{
|
||||||
padding: { top: 0, bottom: 1, left: 1, right: 1 },
|
padding: { top: 0, bottom: 1, left: 1, right: 1 },
|
||||||
margin: { top: 1, bottom: 0 },
|
margin: { top: 1, bottom: 0 },
|
||||||
borderColor: "white",
|
borderColor: 'white',
|
||||||
borderStyle: "round",
|
borderStyle: 'round'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -831,15 +831,15 @@ async function addTask(
|
|||||||
// System Prompt - Enhanced for dependency awareness
|
// System Prompt - Enhanced for dependency awareness
|
||||||
const systemPrompt =
|
const systemPrompt =
|
||||||
"You are a helpful assistant that creates well-structured tasks for a software development project. Generate a single new task based on the user's description, adhering strictly to the provided JSON schema. Pay special attention to dependencies between tasks, ensuring the new task correctly references any tasks it depends on.\n\n" +
|
"You are a helpful assistant that creates well-structured tasks for a software development project. Generate a single new task based on the user's description, adhering strictly to the provided JSON schema. Pay special attention to dependencies between tasks, ensuring the new task correctly references any tasks it depends on.\n\n" +
|
||||||
"When determining dependencies for a new task, follow these principles:\n" +
|
'When determining dependencies for a new task, follow these principles:\n' +
|
||||||
"1. Select dependencies based on logical requirements - what must be completed before this task can begin.\n" +
|
'1. Select dependencies based on logical requirements - what must be completed before this task can begin.\n' +
|
||||||
"2. Prioritize task dependencies that are semantically related to the functionality being built.\n" +
|
'2. Prioritize task dependencies that are semantically related to the functionality being built.\n' +
|
||||||
"3. Consider both direct dependencies (immediately prerequisite) and indirect dependencies.\n" +
|
'3. Consider both direct dependencies (immediately prerequisite) and indirect dependencies.\n' +
|
||||||
"4. Avoid adding unnecessary dependencies - only include tasks that are genuinely prerequisite.\n" +
|
'4. Avoid adding unnecessary dependencies - only include tasks that are genuinely prerequisite.\n' +
|
||||||
"5. Consider the current status of tasks - prefer completed tasks as dependencies when possible.\n" +
|
'5. Consider the current status of tasks - prefer completed tasks as dependencies when possible.\n' +
|
||||||
"6. Pay special attention to foundation tasks (1-5) but don't automatically include them without reason.\n" +
|
"6. Pay special attention to foundation tasks (1-5) but don't automatically include them without reason.\n" +
|
||||||
"7. Recent tasks (higher ID numbers) may be more relevant for newer functionality.\n\n" +
|
'7. Recent tasks (higher ID numbers) may be more relevant for newer functionality.\n\n' +
|
||||||
"The dependencies array should contain task IDs (numbers) of prerequisite tasks.\n";
|
'The dependencies array should contain task IDs (numbers) of prerequisite tasks.\n';
|
||||||
|
|
||||||
// Task Structure Description (for user prompt)
|
// Task Structure Description (for user prompt)
|
||||||
const taskStructureDesc = `
|
const taskStructureDesc = `
|
||||||
@@ -853,7 +853,7 @@ async function addTask(
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
// Add any manually provided details to the prompt for context
|
// Add any manually provided details to the prompt for context
|
||||||
let contextFromArgs = "";
|
let contextFromArgs = '';
|
||||||
if (manualTaskData?.title)
|
if (manualTaskData?.title)
|
||||||
contextFromArgs += `\n- Suggested Title: "${manualTaskData.title}"`;
|
contextFromArgs += `\n- Suggested Title: "${manualTaskData.title}"`;
|
||||||
if (manualTaskData?.description)
|
if (manualTaskData?.description)
|
||||||
@@ -867,7 +867,7 @@ async function addTask(
|
|||||||
const userPrompt = `You are generating the details for Task #${newTaskId}. Based on the user's request: "${prompt}", create a comprehensive new task for a software development project.
|
const userPrompt = `You are generating the details for Task #${newTaskId}. Based on the user's request: "${prompt}", create a comprehensive new task for a software development project.
|
||||||
|
|
||||||
${contextTasks}
|
${contextTasks}
|
||||||
${contextFromArgs ? `\nConsider these additional details provided by the user:${contextFromArgs}` : ""}
|
${contextFromArgs ? `\nConsider these additional details provided by the user:${contextFromArgs}` : ''}
|
||||||
|
|
||||||
Based on the information about existing tasks provided above, include appropriate dependencies in the "dependencies" array. Only include task IDs that this new task directly depends on.
|
Based on the information about existing tasks provided above, include appropriate dependencies in the "dependencies" array. Only include task IDs that this new task directly depends on.
|
||||||
|
|
||||||
@@ -878,15 +878,15 @@ async function addTask(
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
// Start the loading indicator - only for text mode
|
// Start the loading indicator - only for text mode
|
||||||
if (outputFormat === "text") {
|
if (outputFormat === 'text') {
|
||||||
loadingIndicator = startLoadingIndicator(
|
loadingIndicator = startLoadingIndicator(
|
||||||
`Generating new task with ${useResearch ? "Research" : "Main"} AI... \n`
|
`Generating new task with ${useResearch ? 'Research' : 'Main'} AI... \n`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const serviceRole = useResearch ? "research" : "main";
|
const serviceRole = useResearch ? 'research' : 'main';
|
||||||
report("DEBUG: Calling generateObjectService...", "debug");
|
report('DEBUG: Calling generateObjectService...', 'debug');
|
||||||
|
|
||||||
aiServiceResponse = await generateObjectService({
|
aiServiceResponse = await generateObjectService({
|
||||||
// Capture the full response
|
// Capture the full response
|
||||||
@@ -894,17 +894,17 @@ async function addTask(
|
|||||||
session: session,
|
session: session,
|
||||||
projectRoot: projectRoot,
|
projectRoot: projectRoot,
|
||||||
schema: AiTaskDataSchema,
|
schema: AiTaskDataSchema,
|
||||||
objectName: "newTaskData",
|
objectName: 'newTaskData',
|
||||||
systemPrompt: systemPrompt,
|
systemPrompt: systemPrompt,
|
||||||
prompt: userPrompt,
|
prompt: userPrompt,
|
||||||
commandName: commandName || "add-task", // Use passed commandName or default
|
commandName: commandName || 'add-task', // Use passed commandName or default
|
||||||
outputType: outputType || (isMCP ? "mcp" : "cli"), // Use passed outputType or derive
|
outputType: outputType || (isMCP ? 'mcp' : 'cli') // Use passed outputType or derive
|
||||||
});
|
});
|
||||||
report("DEBUG: generateObjectService returned successfully.", "debug");
|
report('DEBUG: generateObjectService returned successfully.', 'debug');
|
||||||
|
|
||||||
if (!aiServiceResponse || !aiServiceResponse.mainResult) {
|
if (!aiServiceResponse || !aiServiceResponse.mainResult) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"AI service did not return the expected object structure."
|
'AI service did not return the expected object structure.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -921,33 +921,33 @@ async function addTask(
|
|||||||
) {
|
) {
|
||||||
taskData = aiServiceResponse.mainResult.object;
|
taskData = aiServiceResponse.mainResult.object;
|
||||||
} else {
|
} else {
|
||||||
throw new Error("AI service did not return a valid task object.");
|
throw new Error('AI service did not return a valid task object.');
|
||||||
}
|
}
|
||||||
|
|
||||||
report("Successfully generated task data from AI.", "success");
|
report('Successfully generated task data from AI.', 'success');
|
||||||
|
|
||||||
// Success! Show checkmark
|
// Success! Show checkmark
|
||||||
if (loadingIndicator) {
|
if (loadingIndicator) {
|
||||||
succeedLoadingIndicator(
|
succeedLoadingIndicator(
|
||||||
loadingIndicator,
|
loadingIndicator,
|
||||||
"Task generated successfully"
|
'Task generated successfully'
|
||||||
);
|
);
|
||||||
loadingIndicator = null; // Clear it
|
loadingIndicator = null; // Clear it
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Failure! Show X
|
// Failure! Show X
|
||||||
if (loadingIndicator) {
|
if (loadingIndicator) {
|
||||||
failLoadingIndicator(loadingIndicator, "AI generation failed");
|
failLoadingIndicator(loadingIndicator, 'AI generation failed');
|
||||||
loadingIndicator = null;
|
loadingIndicator = null;
|
||||||
}
|
}
|
||||||
report(
|
report(
|
||||||
`DEBUG: generateObjectService caught error: ${error.message}`,
|
`DEBUG: generateObjectService caught error: ${error.message}`,
|
||||||
"debug"
|
'debug'
|
||||||
);
|
);
|
||||||
report(`Error generating task with AI: ${error.message}`, "error");
|
report(`Error generating task with AI: ${error.message}`, 'error');
|
||||||
throw error; // Re-throw error after logging
|
throw error; // Re-throw error after logging
|
||||||
} finally {
|
} finally {
|
||||||
report("DEBUG: generateObjectService finally block reached.", "debug");
|
report('DEBUG: generateObjectService finally block reached.', 'debug');
|
||||||
// Clean up if somehow still running
|
// Clean up if somehow still running
|
||||||
if (loadingIndicator) {
|
if (loadingIndicator) {
|
||||||
stopLoadingIndicator(loadingIndicator);
|
stopLoadingIndicator(loadingIndicator);
|
||||||
@@ -961,14 +961,14 @@ async function addTask(
|
|||||||
id: newTaskId,
|
id: newTaskId,
|
||||||
title: taskData.title,
|
title: taskData.title,
|
||||||
description: taskData.description,
|
description: taskData.description,
|
||||||
details: taskData.details || "",
|
details: taskData.details || '',
|
||||||
testStrategy: taskData.testStrategy || "",
|
testStrategy: taskData.testStrategy || '',
|
||||||
status: "pending",
|
status: 'pending',
|
||||||
dependencies: taskData.dependencies?.length
|
dependencies: taskData.dependencies?.length
|
||||||
? taskData.dependencies
|
? taskData.dependencies
|
||||||
: numericDependencies, // Use AI-suggested dependencies if available, fallback to manually specified
|
: numericDependencies, // Use AI-suggested dependencies if available, fallback to manually specified
|
||||||
priority: effectivePriority,
|
priority: effectivePriority,
|
||||||
subtasks: [], // Initialize with empty subtasks array
|
subtasks: [] // Initialize with empty subtasks array
|
||||||
};
|
};
|
||||||
|
|
||||||
// Additional check: validate all dependencies in the AI response
|
// Additional check: validate all dependencies in the AI response
|
||||||
@@ -980,8 +980,8 @@ async function addTask(
|
|||||||
|
|
||||||
if (!allValidDeps) {
|
if (!allValidDeps) {
|
||||||
report(
|
report(
|
||||||
"AI suggested invalid dependencies. Filtering them out...",
|
'AI suggested invalid dependencies. Filtering them out...',
|
||||||
"warn"
|
'warn'
|
||||||
);
|
);
|
||||||
newTask.dependencies = taskData.dependencies.filter((depId) => {
|
newTask.dependencies = taskData.dependencies.filter((depId) => {
|
||||||
const numDepId = parseInt(depId, 10);
|
const numDepId = parseInt(depId, 10);
|
||||||
@@ -993,48 +993,48 @@ async function addTask(
|
|||||||
// Add the task to the tasks array
|
// Add the task to the tasks array
|
||||||
data.tasks.push(newTask);
|
data.tasks.push(newTask);
|
||||||
|
|
||||||
report("DEBUG: Writing tasks.json...", "debug");
|
report('DEBUG: Writing tasks.json...', 'debug');
|
||||||
// Write the updated tasks to the file
|
// Write the updated tasks to the file
|
||||||
writeJSON(tasksPath, data);
|
writeJSON(tasksPath, data);
|
||||||
report("DEBUG: tasks.json written.", "debug");
|
report('DEBUG: tasks.json written.', 'debug');
|
||||||
|
|
||||||
// Generate markdown task files
|
// Generate markdown task files
|
||||||
report("Generating task files...", "info");
|
report('Generating task files...', 'info');
|
||||||
report("DEBUG: Calling generateTaskFiles...", "debug");
|
report('DEBUG: Calling generateTaskFiles...', 'debug');
|
||||||
// Pass mcpLog if available to generateTaskFiles
|
// Pass mcpLog if available to generateTaskFiles
|
||||||
await generateTaskFiles(tasksPath, path.dirname(tasksPath), { mcpLog });
|
await generateTaskFiles(tasksPath, path.dirname(tasksPath), { mcpLog });
|
||||||
report("DEBUG: generateTaskFiles finished.", "debug");
|
report('DEBUG: generateTaskFiles finished.', 'debug');
|
||||||
|
|
||||||
// Show success message - only for text output (CLI)
|
// Show success message - only for text output (CLI)
|
||||||
if (outputFormat === "text") {
|
if (outputFormat === 'text') {
|
||||||
const table = new Table({
|
const table = new Table({
|
||||||
head: [
|
head: [
|
||||||
chalk.cyan.bold("ID"),
|
chalk.cyan.bold('ID'),
|
||||||
chalk.cyan.bold("Title"),
|
chalk.cyan.bold('Title'),
|
||||||
chalk.cyan.bold("Description"),
|
chalk.cyan.bold('Description')
|
||||||
],
|
],
|
||||||
colWidths: [5, 30, 50], // Adjust widths as needed
|
colWidths: [5, 30, 50] // Adjust widths as needed
|
||||||
});
|
});
|
||||||
|
|
||||||
table.push([
|
table.push([
|
||||||
newTask.id,
|
newTask.id,
|
||||||
truncate(newTask.title, 27),
|
truncate(newTask.title, 27),
|
||||||
truncate(newTask.description, 47),
|
truncate(newTask.description, 47)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
console.log(chalk.green("✓ New task created successfully:"));
|
console.log(chalk.green('✓ New task created successfully:'));
|
||||||
console.log(table.toString());
|
console.log(table.toString());
|
||||||
|
|
||||||
// Helper to get priority color
|
// Helper to get priority color
|
||||||
const getPriorityColor = (p) => {
|
const getPriorityColor = (p) => {
|
||||||
switch (p?.toLowerCase()) {
|
switch (p?.toLowerCase()) {
|
||||||
case "high":
|
case 'high':
|
||||||
return "red";
|
return 'red';
|
||||||
case "low":
|
case 'low':
|
||||||
return "gray";
|
return 'gray';
|
||||||
case "medium":
|
case 'medium':
|
||||||
default:
|
default:
|
||||||
return "yellow";
|
return 'yellow';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1058,49 +1058,49 @@ async function addTask(
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Prepare dependency display string
|
// Prepare dependency display string
|
||||||
let dependencyDisplay = "";
|
let dependencyDisplay = '';
|
||||||
if (newTask.dependencies.length > 0) {
|
if (newTask.dependencies.length > 0) {
|
||||||
dependencyDisplay = chalk.white("Dependencies:") + "\n";
|
dependencyDisplay = chalk.white('Dependencies:') + '\n';
|
||||||
newTask.dependencies.forEach((dep) => {
|
newTask.dependencies.forEach((dep) => {
|
||||||
const isAiAdded = aiAddedDeps.includes(dep);
|
const isAiAdded = aiAddedDeps.includes(dep);
|
||||||
const depType = isAiAdded ? chalk.yellow(" (AI suggested)") : "";
|
const depType = isAiAdded ? chalk.yellow(' (AI suggested)') : '';
|
||||||
dependencyDisplay +=
|
dependencyDisplay +=
|
||||||
chalk.white(
|
chalk.white(
|
||||||
` - ${dep}: ${depTitles[dep] || "Unknown task"}${depType}`
|
` - ${dep}: ${depTitles[dep] || 'Unknown task'}${depType}`
|
||||||
) + "\n";
|
) + '\n';
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
dependencyDisplay = chalk.white("Dependencies: None") + "\n";
|
dependencyDisplay = chalk.white('Dependencies: None') + '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add info about removed dependencies if any
|
// Add info about removed dependencies if any
|
||||||
if (aiRemovedDeps.length > 0) {
|
if (aiRemovedDeps.length > 0) {
|
||||||
dependencyDisplay +=
|
dependencyDisplay +=
|
||||||
chalk.gray("\nUser-specified dependencies that were not used:") +
|
chalk.gray('\nUser-specified dependencies that were not used:') +
|
||||||
"\n";
|
'\n';
|
||||||
aiRemovedDeps.forEach((dep) => {
|
aiRemovedDeps.forEach((dep) => {
|
||||||
const depTask = data.tasks.find((t) => t.id === dep);
|
const depTask = data.tasks.find((t) => t.id === dep);
|
||||||
const title = depTask ? truncate(depTask.title, 30) : "Unknown task";
|
const title = depTask ? truncate(depTask.title, 30) : 'Unknown task';
|
||||||
dependencyDisplay += chalk.gray(` - ${dep}: ${title}`) + "\n";
|
dependencyDisplay += chalk.gray(` - ${dep}: ${title}`) + '\n';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add dependency analysis summary
|
// Add dependency analysis summary
|
||||||
let dependencyAnalysis = "";
|
let dependencyAnalysis = '';
|
||||||
if (aiAddedDeps.length > 0 || aiRemovedDeps.length > 0) {
|
if (aiAddedDeps.length > 0 || aiRemovedDeps.length > 0) {
|
||||||
dependencyAnalysis =
|
dependencyAnalysis =
|
||||||
"\n" + chalk.white.bold("Dependency Analysis:") + "\n";
|
'\n' + chalk.white.bold('Dependency Analysis:') + '\n';
|
||||||
if (aiAddedDeps.length > 0) {
|
if (aiAddedDeps.length > 0) {
|
||||||
dependencyAnalysis +=
|
dependencyAnalysis +=
|
||||||
chalk.green(
|
chalk.green(
|
||||||
`AI identified ${aiAddedDeps.length} additional dependencies`
|
`AI identified ${aiAddedDeps.length} additional dependencies`
|
||||||
) + "\n";
|
) + '\n';
|
||||||
}
|
}
|
||||||
if (aiRemovedDeps.length > 0) {
|
if (aiRemovedDeps.length > 0) {
|
||||||
dependencyAnalysis +=
|
dependencyAnalysis +=
|
||||||
chalk.yellow(
|
chalk.yellow(
|
||||||
`AI excluded ${aiRemovedDeps.length} user-provided dependencies`
|
`AI excluded ${aiRemovedDeps.length} user-provided dependencies`
|
||||||
) + "\n";
|
) + '\n';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1108,32 +1108,32 @@ async function addTask(
|
|||||||
console.log(
|
console.log(
|
||||||
boxen(
|
boxen(
|
||||||
chalk.white.bold(`Task ${newTaskId} Created Successfully`) +
|
chalk.white.bold(`Task ${newTaskId} Created Successfully`) +
|
||||||
"\n\n" +
|
'\n\n' +
|
||||||
chalk.white(`Title: ${newTask.title}`) +
|
chalk.white(`Title: ${newTask.title}`) +
|
||||||
"\n" +
|
'\n' +
|
||||||
chalk.white(`Status: ${getStatusWithColor(newTask.status)}`) +
|
chalk.white(`Status: ${getStatusWithColor(newTask.status)}`) +
|
||||||
"\n" +
|
'\n' +
|
||||||
chalk.white(
|
chalk.white(
|
||||||
`Priority: ${chalk[getPriorityColor(newTask.priority)](newTask.priority)}`
|
`Priority: ${chalk[getPriorityColor(newTask.priority)](newTask.priority)}`
|
||||||
) +
|
) +
|
||||||
"\n\n" +
|
'\n\n' +
|
||||||
dependencyDisplay +
|
dependencyDisplay +
|
||||||
dependencyAnalysis +
|
dependencyAnalysis +
|
||||||
"\n" +
|
'\n' +
|
||||||
chalk.white.bold("Next Steps:") +
|
chalk.white.bold('Next Steps:') +
|
||||||
"\n" +
|
'\n' +
|
||||||
chalk.cyan(
|
chalk.cyan(
|
||||||
`1. Run ${chalk.yellow(`task-master show ${newTaskId}`)} to see complete task details`
|
`1. Run ${chalk.yellow(`task-master show ${newTaskId}`)} to see complete task details`
|
||||||
) +
|
) +
|
||||||
"\n" +
|
'\n' +
|
||||||
chalk.cyan(
|
chalk.cyan(
|
||||||
`2. Run ${chalk.yellow(`task-master set-status --id=${newTaskId} --status=in-progress`)} to start working on it`
|
`2. Run ${chalk.yellow(`task-master set-status --id=${newTaskId} --status=in-progress`)} to start working on it`
|
||||||
) +
|
) +
|
||||||
"\n" +
|
'\n' +
|
||||||
chalk.cyan(
|
chalk.cyan(
|
||||||
`3. Run ${chalk.yellow(`task-master expand --id=${newTaskId}`)} to break it down into subtasks`
|
`3. Run ${chalk.yellow(`task-master expand --id=${newTaskId}`)} to break it down into subtasks`
|
||||||
),
|
),
|
||||||
{ padding: 1, borderColor: "green", borderStyle: "round" }
|
{ padding: 1, borderColor: 'green', borderStyle: 'round' }
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1141,19 +1141,19 @@ async function addTask(
|
|||||||
if (
|
if (
|
||||||
aiServiceResponse &&
|
aiServiceResponse &&
|
||||||
aiServiceResponse.telemetryData &&
|
aiServiceResponse.telemetryData &&
|
||||||
(outputType === "cli" || outputType === "text")
|
(outputType === 'cli' || outputType === 'text')
|
||||||
) {
|
) {
|
||||||
displayAiUsageSummary(aiServiceResponse.telemetryData, "cli");
|
displayAiUsageSummary(aiServiceResponse.telemetryData, 'cli');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
report(
|
report(
|
||||||
`DEBUG: Returning new task ID: ${newTaskId} and telemetry.`,
|
`DEBUG: Returning new task ID: ${newTaskId} and telemetry.`,
|
||||||
"debug"
|
'debug'
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
newTaskId: newTaskId,
|
newTaskId: newTaskId,
|
||||||
telemetryData: aiServiceResponse ? aiServiceResponse.telemetryData : null,
|
telemetryData: aiServiceResponse ? aiServiceResponse.telemetryData : null
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Stop any loading indicator on error
|
// Stop any loading indicator on error
|
||||||
@@ -1161,8 +1161,8 @@ async function addTask(
|
|||||||
stopLoadingIndicator(loadingIndicator);
|
stopLoadingIndicator(loadingIndicator);
|
||||||
}
|
}
|
||||||
|
|
||||||
report(`Error adding task: ${error.message}`, "error");
|
report(`Error adding task: ${error.message}`, 'error');
|
||||||
if (outputFormat === "text") {
|
if (outputFormat === 'text') {
|
||||||
console.error(chalk.red(`Error: ${error.message}`));
|
console.error(chalk.red(`Error: ${error.message}`));
|
||||||
}
|
}
|
||||||
// In MCP mode, we let the direct function handler catch and format
|
// In MCP mode, we let the direct function handler catch and format
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import path from "path";
|
import path from 'path';
|
||||||
import chalk from "chalk";
|
import chalk from 'chalk';
|
||||||
import boxen from "boxen";
|
import boxen from 'boxen';
|
||||||
import Table from "cli-table3";
|
import Table from 'cli-table3';
|
||||||
|
|
||||||
import { log, readJSON, writeJSON, truncate, isSilentMode } from "../utils.js";
|
import { log, readJSON, writeJSON, truncate, isSilentMode } from '../utils.js';
|
||||||
import { displayBanner } from "../ui.js";
|
import { displayBanner } from '../ui.js';
|
||||||
import generateTaskFiles from "./generate-task-files.js";
|
import generateTaskFiles from './generate-task-files.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear subtasks from specified tasks
|
* Clear subtasks from specified tasks
|
||||||
@@ -13,58 +13,58 @@ import generateTaskFiles from "./generate-task-files.js";
|
|||||||
* @param {string} taskIds - Task IDs to clear subtasks from
|
* @param {string} taskIds - Task IDs to clear subtasks from
|
||||||
*/
|
*/
|
||||||
function clearSubtasks(tasksPath, taskIds) {
|
function clearSubtasks(tasksPath, taskIds) {
|
||||||
log("info", `Reading tasks from ${tasksPath}...`);
|
log('info', `Reading tasks from ${tasksPath}...`);
|
||||||
const data = readJSON(tasksPath);
|
const data = readJSON(tasksPath);
|
||||||
if (!data || !data.tasks) {
|
if (!data || !data.tasks) {
|
||||||
log("error", "No valid tasks found.");
|
log('error', 'No valid tasks found.');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isSilentMode()) {
|
if (!isSilentMode()) {
|
||||||
console.log(
|
console.log(
|
||||||
boxen(chalk.white.bold("Clearing Subtasks"), {
|
boxen(chalk.white.bold('Clearing Subtasks'), {
|
||||||
padding: 1,
|
padding: 1,
|
||||||
borderColor: "blue",
|
borderColor: 'blue',
|
||||||
borderStyle: "round",
|
borderStyle: 'round',
|
||||||
margin: { top: 1, bottom: 1 },
|
margin: { top: 1, bottom: 1 }
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle multiple task IDs (comma-separated)
|
// Handle multiple task IDs (comma-separated)
|
||||||
const taskIdArray = taskIds.split(",").map((id) => id.trim());
|
const taskIdArray = taskIds.split(',').map((id) => id.trim());
|
||||||
let clearedCount = 0;
|
let clearedCount = 0;
|
||||||
|
|
||||||
// Create a summary table for the cleared subtasks
|
// Create a summary table for the cleared subtasks
|
||||||
const summaryTable = new Table({
|
const summaryTable = new Table({
|
||||||
head: [
|
head: [
|
||||||
chalk.cyan.bold("Task ID"),
|
chalk.cyan.bold('Task ID'),
|
||||||
chalk.cyan.bold("Task Title"),
|
chalk.cyan.bold('Task Title'),
|
||||||
chalk.cyan.bold("Subtasks Cleared"),
|
chalk.cyan.bold('Subtasks Cleared')
|
||||||
],
|
],
|
||||||
colWidths: [10, 50, 20],
|
colWidths: [10, 50, 20],
|
||||||
style: { head: [], border: [] },
|
style: { head: [], border: [] }
|
||||||
});
|
});
|
||||||
|
|
||||||
taskIdArray.forEach((taskId) => {
|
taskIdArray.forEach((taskId) => {
|
||||||
const id = parseInt(taskId, 10);
|
const id = parseInt(taskId, 10);
|
||||||
if (isNaN(id)) {
|
if (isNaN(id)) {
|
||||||
log("error", `Invalid task ID: ${taskId}`);
|
log('error', `Invalid task ID: ${taskId}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const task = data.tasks.find((t) => t.id === id);
|
const task = data.tasks.find((t) => t.id === id);
|
||||||
if (!task) {
|
if (!task) {
|
||||||
log("error", `Task ${id} not found`);
|
log('error', `Task ${id} not found`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!task.subtasks || task.subtasks.length === 0) {
|
if (!task.subtasks || task.subtasks.length === 0) {
|
||||||
log("info", `Task ${id} has no subtasks to clear`);
|
log('info', `Task ${id} has no subtasks to clear`);
|
||||||
summaryTable.push([
|
summaryTable.push([
|
||||||
id.toString(),
|
id.toString(),
|
||||||
truncate(task.title, 47),
|
truncate(task.title, 47),
|
||||||
chalk.yellow("No subtasks"),
|
chalk.yellow('No subtasks')
|
||||||
]);
|
]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -72,12 +72,12 @@ function clearSubtasks(tasksPath, taskIds) {
|
|||||||
const subtaskCount = task.subtasks.length;
|
const subtaskCount = task.subtasks.length;
|
||||||
task.subtasks = [];
|
task.subtasks = [];
|
||||||
clearedCount++;
|
clearedCount++;
|
||||||
log("info", `Cleared ${subtaskCount} subtasks from task ${id}`);
|
log('info', `Cleared ${subtaskCount} subtasks from task ${id}`);
|
||||||
|
|
||||||
summaryTable.push([
|
summaryTable.push([
|
||||||
id.toString(),
|
id.toString(),
|
||||||
truncate(task.title, 47),
|
truncate(task.title, 47),
|
||||||
chalk.green(`${subtaskCount} subtasks cleared`),
|
chalk.green(`${subtaskCount} subtasks cleared`)
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -87,18 +87,18 @@ function clearSubtasks(tasksPath, taskIds) {
|
|||||||
// Show summary table
|
// Show summary table
|
||||||
if (!isSilentMode()) {
|
if (!isSilentMode()) {
|
||||||
console.log(
|
console.log(
|
||||||
boxen(chalk.white.bold("Subtask Clearing Summary:"), {
|
boxen(chalk.white.bold('Subtask Clearing Summary:'), {
|
||||||
padding: { left: 2, right: 2, top: 0, bottom: 0 },
|
padding: { left: 2, right: 2, top: 0, bottom: 0 },
|
||||||
margin: { top: 1, bottom: 0 },
|
margin: { top: 1, bottom: 0 },
|
||||||
borderColor: "blue",
|
borderColor: 'blue',
|
||||||
borderStyle: "round",
|
borderStyle: 'round'
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
console.log(summaryTable.toString());
|
console.log(summaryTable.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Regenerate task files to reflect changes
|
// Regenerate task files to reflect changes
|
||||||
log("info", "Regenerating task files...");
|
log('info', 'Regenerating task files...');
|
||||||
generateTaskFiles(tasksPath, path.dirname(tasksPath));
|
generateTaskFiles(tasksPath, path.dirname(tasksPath));
|
||||||
|
|
||||||
// Success message
|
// Success message
|
||||||
@@ -110,9 +110,9 @@ function clearSubtasks(tasksPath, taskIds) {
|
|||||||
),
|
),
|
||||||
{
|
{
|
||||||
padding: 1,
|
padding: 1,
|
||||||
borderColor: "green",
|
borderColor: 'green',
|
||||||
borderStyle: "round",
|
borderStyle: 'round',
|
||||||
margin: { top: 1 },
|
margin: { top: 1 }
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -120,15 +120,15 @@ function clearSubtasks(tasksPath, taskIds) {
|
|||||||
// Next steps suggestion
|
// Next steps suggestion
|
||||||
console.log(
|
console.log(
|
||||||
boxen(
|
boxen(
|
||||||
chalk.white.bold("Next Steps:") +
|
chalk.white.bold('Next Steps:') +
|
||||||
"\n\n" +
|
'\n\n' +
|
||||||
`${chalk.cyan("1.")} Run ${chalk.yellow("task-master expand --id=<id>")} to generate new subtasks\n` +
|
`${chalk.cyan('1.')} Run ${chalk.yellow('task-master expand --id=<id>')} to generate new subtasks\n` +
|
||||||
`${chalk.cyan("2.")} Run ${chalk.yellow("task-master list --with-subtasks")} to verify changes`,
|
`${chalk.cyan('2.')} Run ${chalk.yellow('task-master list --with-subtasks')} to verify changes`,
|
||||||
{
|
{
|
||||||
padding: 1,
|
padding: 1,
|
||||||
borderColor: "cyan",
|
borderColor: 'cyan',
|
||||||
borderStyle: "round",
|
borderStyle: 'round',
|
||||||
margin: { top: 1 },
|
margin: { top: 1 }
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -136,11 +136,11 @@ function clearSubtasks(tasksPath, taskIds) {
|
|||||||
} else {
|
} else {
|
||||||
if (!isSilentMode()) {
|
if (!isSilentMode()) {
|
||||||
console.log(
|
console.log(
|
||||||
boxen(chalk.yellow("No subtasks were cleared"), {
|
boxen(chalk.yellow('No subtasks were cleared'), {
|
||||||
padding: 1,
|
padding: 1,
|
||||||
borderColor: "yellow",
|
borderColor: 'yellow',
|
||||||
borderStyle: "round",
|
borderStyle: 'round',
|
||||||
margin: { top: 1 },
|
margin: { top: 1 }
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
import chalk from "chalk";
|
import chalk from 'chalk';
|
||||||
import boxen from "boxen";
|
import boxen from 'boxen';
|
||||||
import Table from "cli-table3";
|
import Table from 'cli-table3';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
log,
|
log,
|
||||||
readJSON,
|
readJSON,
|
||||||
truncate,
|
truncate,
|
||||||
readComplexityReport,
|
readComplexityReport,
|
||||||
addComplexityToTask,
|
addComplexityToTask
|
||||||
} from "../utils.js";
|
} from '../utils.js';
|
||||||
import findNextTask from "./find-next-task.js";
|
import findNextTask from './find-next-task.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
displayBanner,
|
displayBanner,
|
||||||
getStatusWithColor,
|
getStatusWithColor,
|
||||||
formatDependenciesWithStatus,
|
formatDependenciesWithStatus,
|
||||||
getComplexityWithColor,
|
getComplexityWithColor,
|
||||||
createProgressBar,
|
createProgressBar
|
||||||
} from "../ui.js";
|
} from '../ui.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List all tasks
|
* List all tasks
|
||||||
@@ -33,7 +33,7 @@ function listTasks(
|
|||||||
statusFilter,
|
statusFilter,
|
||||||
reportPath = null,
|
reportPath = null,
|
||||||
withSubtasks = false,
|
withSubtasks = false,
|
||||||
outputFormat = "text"
|
outputFormat = 'text'
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const data = readJSON(tasksPath); // Reads the whole tasks.json
|
const data = readJSON(tasksPath); // Reads the whole tasks.json
|
||||||
@@ -50,7 +50,7 @@ function listTasks(
|
|||||||
|
|
||||||
// Filter tasks by status if specified
|
// Filter tasks by status if specified
|
||||||
const filteredTasks =
|
const filteredTasks =
|
||||||
statusFilter && statusFilter.toLowerCase() !== "all" // <-- Added check for 'all'
|
statusFilter && statusFilter.toLowerCase() !== 'all' // <-- Added check for 'all'
|
||||||
? data.tasks.filter(
|
? data.tasks.filter(
|
||||||
(task) =>
|
(task) =>
|
||||||
task.status &&
|
task.status &&
|
||||||
@@ -61,7 +61,7 @@ function listTasks(
|
|||||||
// Calculate completion statistics
|
// Calculate completion statistics
|
||||||
const totalTasks = data.tasks.length;
|
const totalTasks = data.tasks.length;
|
||||||
const completedTasks = data.tasks.filter(
|
const completedTasks = data.tasks.filter(
|
||||||
(task) => task.status === "done" || task.status === "completed"
|
(task) => task.status === 'done' || task.status === 'completed'
|
||||||
).length;
|
).length;
|
||||||
const completionPercentage =
|
const completionPercentage =
|
||||||
totalTasks > 0 ? (completedTasks / totalTasks) * 100 : 0;
|
totalTasks > 0 ? (completedTasks / totalTasks) * 100 : 0;
|
||||||
@@ -69,19 +69,19 @@ function listTasks(
|
|||||||
// Count statuses for tasks
|
// Count statuses for tasks
|
||||||
const doneCount = completedTasks;
|
const doneCount = completedTasks;
|
||||||
const inProgressCount = data.tasks.filter(
|
const inProgressCount = data.tasks.filter(
|
||||||
(task) => task.status === "in-progress"
|
(task) => task.status === 'in-progress'
|
||||||
).length;
|
).length;
|
||||||
const pendingCount = data.tasks.filter(
|
const pendingCount = data.tasks.filter(
|
||||||
(task) => task.status === "pending"
|
(task) => task.status === 'pending'
|
||||||
).length;
|
).length;
|
||||||
const blockedCount = data.tasks.filter(
|
const blockedCount = data.tasks.filter(
|
||||||
(task) => task.status === "blocked"
|
(task) => task.status === 'blocked'
|
||||||
).length;
|
).length;
|
||||||
const deferredCount = data.tasks.filter(
|
const deferredCount = data.tasks.filter(
|
||||||
(task) => task.status === "deferred"
|
(task) => task.status === 'deferred'
|
||||||
).length;
|
).length;
|
||||||
const cancelledCount = data.tasks.filter(
|
const cancelledCount = data.tasks.filter(
|
||||||
(task) => task.status === "cancelled"
|
(task) => task.status === 'cancelled'
|
||||||
).length;
|
).length;
|
||||||
|
|
||||||
// Count subtasks and their statuses
|
// Count subtasks and their statuses
|
||||||
@@ -97,22 +97,22 @@ function listTasks(
|
|||||||
if (task.subtasks && task.subtasks.length > 0) {
|
if (task.subtasks && task.subtasks.length > 0) {
|
||||||
totalSubtasks += task.subtasks.length;
|
totalSubtasks += task.subtasks.length;
|
||||||
completedSubtasks += task.subtasks.filter(
|
completedSubtasks += task.subtasks.filter(
|
||||||
(st) => st.status === "done" || st.status === "completed"
|
(st) => st.status === 'done' || st.status === 'completed'
|
||||||
).length;
|
).length;
|
||||||
inProgressSubtasks += task.subtasks.filter(
|
inProgressSubtasks += task.subtasks.filter(
|
||||||
(st) => st.status === "in-progress"
|
(st) => st.status === 'in-progress'
|
||||||
).length;
|
).length;
|
||||||
pendingSubtasks += task.subtasks.filter(
|
pendingSubtasks += task.subtasks.filter(
|
||||||
(st) => st.status === "pending"
|
(st) => st.status === 'pending'
|
||||||
).length;
|
).length;
|
||||||
blockedSubtasks += task.subtasks.filter(
|
blockedSubtasks += task.subtasks.filter(
|
||||||
(st) => st.status === "blocked"
|
(st) => st.status === 'blocked'
|
||||||
).length;
|
).length;
|
||||||
deferredSubtasks += task.subtasks.filter(
|
deferredSubtasks += task.subtasks.filter(
|
||||||
(st) => st.status === "deferred"
|
(st) => st.status === 'deferred'
|
||||||
).length;
|
).length;
|
||||||
cancelledSubtasks += task.subtasks.filter(
|
cancelledSubtasks += task.subtasks.filter(
|
||||||
(st) => st.status === "cancelled"
|
(st) => st.status === 'cancelled'
|
||||||
).length;
|
).length;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -121,7 +121,7 @@ function listTasks(
|
|||||||
totalSubtasks > 0 ? (completedSubtasks / totalSubtasks) * 100 : 0;
|
totalSubtasks > 0 ? (completedSubtasks / totalSubtasks) * 100 : 0;
|
||||||
|
|
||||||
// For JSON output, return structured data
|
// For JSON output, return structured data
|
||||||
if (outputFormat === "json") {
|
if (outputFormat === 'json') {
|
||||||
// *** Modification: Remove 'details' field for JSON output ***
|
// *** Modification: Remove 'details' field for JSON output ***
|
||||||
const tasksWithoutDetails = filteredTasks.map((task) => {
|
const tasksWithoutDetails = filteredTasks.map((task) => {
|
||||||
// <-- USES filteredTasks!
|
// <-- USES filteredTasks!
|
||||||
@@ -141,7 +141,7 @@ function listTasks(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
tasks: tasksWithoutDetails, // <--- THIS IS THE ARRAY BEING RETURNED
|
tasks: tasksWithoutDetails, // <--- THIS IS THE ARRAY BEING RETURNED
|
||||||
filter: statusFilter || "all", // Return the actual filter used
|
filter: statusFilter || 'all', // Return the actual filter used
|
||||||
stats: {
|
stats: {
|
||||||
total: totalTasks,
|
total: totalTasks,
|
||||||
completed: doneCount,
|
completed: doneCount,
|
||||||
@@ -159,9 +159,9 @@ function listTasks(
|
|||||||
blocked: blockedSubtasks,
|
blocked: blockedSubtasks,
|
||||||
deferred: deferredSubtasks,
|
deferred: deferredSubtasks,
|
||||||
cancelled: cancelledSubtasks,
|
cancelled: cancelledSubtasks,
|
||||||
completionPercentage: subtaskCompletionPercentage,
|
completionPercentage: subtaskCompletionPercentage
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,22 +169,22 @@ function listTasks(
|
|||||||
|
|
||||||
// Calculate status breakdowns as percentages of total
|
// Calculate status breakdowns as percentages of total
|
||||||
const taskStatusBreakdown = {
|
const taskStatusBreakdown = {
|
||||||
"in-progress": totalTasks > 0 ? (inProgressCount / totalTasks) * 100 : 0,
|
'in-progress': totalTasks > 0 ? (inProgressCount / totalTasks) * 100 : 0,
|
||||||
pending: totalTasks > 0 ? (pendingCount / totalTasks) * 100 : 0,
|
pending: totalTasks > 0 ? (pendingCount / totalTasks) * 100 : 0,
|
||||||
blocked: totalTasks > 0 ? (blockedCount / totalTasks) * 100 : 0,
|
blocked: totalTasks > 0 ? (blockedCount / totalTasks) * 100 : 0,
|
||||||
deferred: totalTasks > 0 ? (deferredCount / totalTasks) * 100 : 0,
|
deferred: totalTasks > 0 ? (deferredCount / totalTasks) * 100 : 0,
|
||||||
cancelled: totalTasks > 0 ? (cancelledCount / totalTasks) * 100 : 0,
|
cancelled: totalTasks > 0 ? (cancelledCount / totalTasks) * 100 : 0
|
||||||
};
|
};
|
||||||
|
|
||||||
const subtaskStatusBreakdown = {
|
const subtaskStatusBreakdown = {
|
||||||
"in-progress":
|
'in-progress':
|
||||||
totalSubtasks > 0 ? (inProgressSubtasks / totalSubtasks) * 100 : 0,
|
totalSubtasks > 0 ? (inProgressSubtasks / totalSubtasks) * 100 : 0,
|
||||||
pending: totalSubtasks > 0 ? (pendingSubtasks / totalSubtasks) * 100 : 0,
|
pending: totalSubtasks > 0 ? (pendingSubtasks / totalSubtasks) * 100 : 0,
|
||||||
blocked: totalSubtasks > 0 ? (blockedSubtasks / totalSubtasks) * 100 : 0,
|
blocked: totalSubtasks > 0 ? (blockedSubtasks / totalSubtasks) * 100 : 0,
|
||||||
deferred:
|
deferred:
|
||||||
totalSubtasks > 0 ? (deferredSubtasks / totalSubtasks) * 100 : 0,
|
totalSubtasks > 0 ? (deferredSubtasks / totalSubtasks) * 100 : 0,
|
||||||
cancelled:
|
cancelled:
|
||||||
totalSubtasks > 0 ? (cancelledSubtasks / totalSubtasks) * 100 : 0,
|
totalSubtasks > 0 ? (cancelledSubtasks / totalSubtasks) * 100 : 0
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create progress bars with status breakdowns
|
// Create progress bars with status breakdowns
|
||||||
@@ -202,21 +202,21 @@ function listTasks(
|
|||||||
// Calculate dependency statistics
|
// Calculate dependency statistics
|
||||||
const completedTaskIds = new Set(
|
const completedTaskIds = new Set(
|
||||||
data.tasks
|
data.tasks
|
||||||
.filter((t) => t.status === "done" || t.status === "completed")
|
.filter((t) => t.status === 'done' || t.status === 'completed')
|
||||||
.map((t) => t.id)
|
.map((t) => t.id)
|
||||||
);
|
);
|
||||||
|
|
||||||
const tasksWithNoDeps = data.tasks.filter(
|
const tasksWithNoDeps = data.tasks.filter(
|
||||||
(t) =>
|
(t) =>
|
||||||
t.status !== "done" &&
|
t.status !== 'done' &&
|
||||||
t.status !== "completed" &&
|
t.status !== 'completed' &&
|
||||||
(!t.dependencies || t.dependencies.length === 0)
|
(!t.dependencies || t.dependencies.length === 0)
|
||||||
).length;
|
).length;
|
||||||
|
|
||||||
const tasksWithAllDepsSatisfied = data.tasks.filter(
|
const tasksWithAllDepsSatisfied = data.tasks.filter(
|
||||||
(t) =>
|
(t) =>
|
||||||
t.status !== "done" &&
|
t.status !== 'done' &&
|
||||||
t.status !== "completed" &&
|
t.status !== 'completed' &&
|
||||||
t.dependencies &&
|
t.dependencies &&
|
||||||
t.dependencies.length > 0 &&
|
t.dependencies.length > 0 &&
|
||||||
t.dependencies.every((depId) => completedTaskIds.has(depId))
|
t.dependencies.every((depId) => completedTaskIds.has(depId))
|
||||||
@@ -224,8 +224,8 @@ function listTasks(
|
|||||||
|
|
||||||
const tasksWithUnsatisfiedDeps = data.tasks.filter(
|
const tasksWithUnsatisfiedDeps = data.tasks.filter(
|
||||||
(t) =>
|
(t) =>
|
||||||
t.status !== "done" &&
|
t.status !== 'done' &&
|
||||||
t.status !== "completed" &&
|
t.status !== 'completed' &&
|
||||||
t.dependencies &&
|
t.dependencies &&
|
||||||
t.dependencies.length > 0 &&
|
t.dependencies.length > 0 &&
|
||||||
!t.dependencies.every((depId) => completedTaskIds.has(depId))
|
!t.dependencies.every((depId) => completedTaskIds.has(depId))
|
||||||
@@ -278,7 +278,7 @@ function listTasks(
|
|||||||
terminalWidth = process.stdout.columns;
|
terminalWidth = process.stdout.columns;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Fallback if columns cannot be determined
|
// Fallback if columns cannot be determined
|
||||||
log("debug", "Could not determine terminal width, using default");
|
log('debug', 'Could not determine terminal width, using default');
|
||||||
}
|
}
|
||||||
// Ensure we have a reasonable default if detection fails
|
// Ensure we have a reasonable default if detection fails
|
||||||
terminalWidth = terminalWidth || 80;
|
terminalWidth = terminalWidth || 80;
|
||||||
@@ -288,35 +288,35 @@ function listTasks(
|
|||||||
|
|
||||||
// Create dashboard content
|
// Create dashboard content
|
||||||
const projectDashboardContent =
|
const projectDashboardContent =
|
||||||
chalk.white.bold("Project Dashboard") +
|
chalk.white.bold('Project Dashboard') +
|
||||||
"\n" +
|
'\n' +
|
||||||
`Tasks Progress: ${chalk.greenBright(taskProgressBar)} ${completionPercentage.toFixed(0)}%\n` +
|
`Tasks Progress: ${chalk.greenBright(taskProgressBar)} ${completionPercentage.toFixed(0)}%\n` +
|
||||||
`Done: ${chalk.green(doneCount)} In Progress: ${chalk.blue(inProgressCount)} Pending: ${chalk.yellow(pendingCount)} Blocked: ${chalk.red(blockedCount)} Deferred: ${chalk.gray(deferredCount)} Cancelled: ${chalk.gray(cancelledCount)}\n\n` +
|
`Done: ${chalk.green(doneCount)} In Progress: ${chalk.blue(inProgressCount)} Pending: ${chalk.yellow(pendingCount)} Blocked: ${chalk.red(blockedCount)} Deferred: ${chalk.gray(deferredCount)} Cancelled: ${chalk.gray(cancelledCount)}\n\n` +
|
||||||
`Subtasks Progress: ${chalk.cyan(subtaskProgressBar)} ${subtaskCompletionPercentage.toFixed(0)}%\n` +
|
`Subtasks Progress: ${chalk.cyan(subtaskProgressBar)} ${subtaskCompletionPercentage.toFixed(0)}%\n` +
|
||||||
`Completed: ${chalk.green(completedSubtasks)}/${totalSubtasks} In Progress: ${chalk.blue(inProgressSubtasks)} Pending: ${chalk.yellow(pendingSubtasks)} Blocked: ${chalk.red(blockedSubtasks)} Deferred: ${chalk.gray(deferredSubtasks)} Cancelled: ${chalk.gray(cancelledSubtasks)}\n\n` +
|
`Completed: ${chalk.green(completedSubtasks)}/${totalSubtasks} In Progress: ${chalk.blue(inProgressSubtasks)} Pending: ${chalk.yellow(pendingSubtasks)} Blocked: ${chalk.red(blockedSubtasks)} Deferred: ${chalk.gray(deferredSubtasks)} Cancelled: ${chalk.gray(cancelledSubtasks)}\n\n` +
|
||||||
chalk.cyan.bold("Priority Breakdown:") +
|
chalk.cyan.bold('Priority Breakdown:') +
|
||||||
"\n" +
|
'\n' +
|
||||||
`${chalk.red("•")} ${chalk.white("High priority:")} ${data.tasks.filter((t) => t.priority === "high").length}\n` +
|
`${chalk.red('•')} ${chalk.white('High priority:')} ${data.tasks.filter((t) => t.priority === 'high').length}\n` +
|
||||||
`${chalk.yellow("•")} ${chalk.white("Medium priority:")} ${data.tasks.filter((t) => t.priority === "medium").length}\n` +
|
`${chalk.yellow('•')} ${chalk.white('Medium priority:')} ${data.tasks.filter((t) => t.priority === 'medium').length}\n` +
|
||||||
`${chalk.green("•")} ${chalk.white("Low priority:")} ${data.tasks.filter((t) => t.priority === "low").length}`;
|
`${chalk.green('•')} ${chalk.white('Low priority:')} ${data.tasks.filter((t) => t.priority === 'low').length}`;
|
||||||
|
|
||||||
const dependencyDashboardContent =
|
const dependencyDashboardContent =
|
||||||
chalk.white.bold("Dependency Status & Next Task") +
|
chalk.white.bold('Dependency Status & Next Task') +
|
||||||
"\n" +
|
'\n' +
|
||||||
chalk.cyan.bold("Dependency Metrics:") +
|
chalk.cyan.bold('Dependency Metrics:') +
|
||||||
"\n" +
|
'\n' +
|
||||||
`${chalk.green("•")} ${chalk.white("Tasks with no dependencies:")} ${tasksWithNoDeps}\n` +
|
`${chalk.green('•')} ${chalk.white('Tasks with no dependencies:')} ${tasksWithNoDeps}\n` +
|
||||||
`${chalk.green("•")} ${chalk.white("Tasks ready to work on:")} ${tasksReadyToWork}\n` +
|
`${chalk.green('•')} ${chalk.white('Tasks ready to work on:')} ${tasksReadyToWork}\n` +
|
||||||
`${chalk.yellow("•")} ${chalk.white("Tasks blocked by dependencies:")} ${tasksWithUnsatisfiedDeps}\n` +
|
`${chalk.yellow('•')} ${chalk.white('Tasks blocked by dependencies:')} ${tasksWithUnsatisfiedDeps}\n` +
|
||||||
`${chalk.magenta("•")} ${chalk.white("Most depended-on task:")} ${mostDependedOnTask ? chalk.cyan(`#${mostDependedOnTaskId} (${maxDependents} dependents)`) : chalk.gray("None")}\n` +
|
`${chalk.magenta('•')} ${chalk.white('Most depended-on task:')} ${mostDependedOnTask ? chalk.cyan(`#${mostDependedOnTaskId} (${maxDependents} dependents)`) : chalk.gray('None')}\n` +
|
||||||
`${chalk.blue("•")} ${chalk.white("Avg dependencies per task:")} ${avgDependenciesPerTask.toFixed(1)}\n\n` +
|
`${chalk.blue('•')} ${chalk.white('Avg dependencies per task:')} ${avgDependenciesPerTask.toFixed(1)}\n\n` +
|
||||||
chalk.cyan.bold("Next Task to Work On:") +
|
chalk.cyan.bold('Next Task to Work On:') +
|
||||||
"\n" +
|
'\n' +
|
||||||
`ID: ${chalk.cyan(nextItem ? nextItem.id : "N/A")} - ${nextItem ? chalk.white.bold(truncate(nextItem.title, 40)) : chalk.yellow("No task available")}
|
`ID: ${chalk.cyan(nextItem ? nextItem.id : 'N/A')} - ${nextItem ? chalk.white.bold(truncate(nextItem.title, 40)) : chalk.yellow('No task available')}
|
||||||
` +
|
` +
|
||||||
`Priority: ${nextItem ? chalk.white(nextItem.priority || "medium") : ""} Dependencies: ${nextItem ? formatDependenciesWithStatus(nextItem.dependencies, data.tasks, true, complexityReport) : ""}
|
`Priority: ${nextItem ? chalk.white(nextItem.priority || 'medium') : ''} Dependencies: ${nextItem ? formatDependenciesWithStatus(nextItem.dependencies, data.tasks, true, complexityReport) : ''}
|
||||||
` +
|
` +
|
||||||
`Complexity: ${nextItem && nextItem.complexityScore ? getComplexityWithColor(nextItem.complexityScore) : chalk.gray("N/A")}`;
|
`Complexity: ${nextItem && nextItem.complexityScore ? getComplexityWithColor(nextItem.complexityScore) : chalk.gray('N/A')}`;
|
||||||
|
|
||||||
// Calculate width for side-by-side display
|
// Calculate width for side-by-side display
|
||||||
// Box borders, padding take approximately 4 chars on each side
|
// Box borders, padding take approximately 4 chars on each side
|
||||||
@@ -336,23 +336,23 @@ function listTasks(
|
|||||||
// Create boxen options with precise widths
|
// Create boxen options with precise widths
|
||||||
const dashboardBox = boxen(projectDashboardContent, {
|
const dashboardBox = boxen(projectDashboardContent, {
|
||||||
padding: 1,
|
padding: 1,
|
||||||
borderColor: "blue",
|
borderColor: 'blue',
|
||||||
borderStyle: "round",
|
borderStyle: 'round',
|
||||||
width: boxContentWidth,
|
width: boxContentWidth,
|
||||||
dimBorder: false,
|
dimBorder: false
|
||||||
});
|
});
|
||||||
|
|
||||||
const dependencyBox = boxen(dependencyDashboardContent, {
|
const dependencyBox = boxen(dependencyDashboardContent, {
|
||||||
padding: 1,
|
padding: 1,
|
||||||
borderColor: "magenta",
|
borderColor: 'magenta',
|
||||||
borderStyle: "round",
|
borderStyle: 'round',
|
||||||
width: boxContentWidth,
|
width: boxContentWidth,
|
||||||
dimBorder: false,
|
dimBorder: false
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create a better side-by-side layout with exact spacing
|
// Create a better side-by-side layout with exact spacing
|
||||||
const dashboardLines = dashboardBox.split("\n");
|
const dashboardLines = dashboardBox.split('\n');
|
||||||
const dependencyLines = dependencyBox.split("\n");
|
const dependencyLines = dependencyBox.split('\n');
|
||||||
|
|
||||||
// Make sure both boxes have the same height
|
// Make sure both boxes have the same height
|
||||||
const maxHeight = Math.max(dashboardLines.length, dependencyLines.length);
|
const maxHeight = Math.max(dashboardLines.length, dependencyLines.length);
|
||||||
@@ -362,35 +362,35 @@ function listTasks(
|
|||||||
const combinedLines = [];
|
const combinedLines = [];
|
||||||
for (let i = 0; i < maxHeight; i++) {
|
for (let i = 0; i < maxHeight; i++) {
|
||||||
// Get the dashboard line (or empty string if we've run out of lines)
|
// Get the dashboard line (or empty string if we've run out of lines)
|
||||||
const dashLine = i < dashboardLines.length ? dashboardLines[i] : "";
|
const dashLine = i < dashboardLines.length ? dashboardLines[i] : '';
|
||||||
// Get the dependency line (or empty string if we've run out of lines)
|
// Get the dependency line (or empty string if we've run out of lines)
|
||||||
const depLine = i < dependencyLines.length ? dependencyLines[i] : "";
|
const depLine = i < dependencyLines.length ? dependencyLines[i] : '';
|
||||||
|
|
||||||
// Remove any trailing spaces from dashLine before padding to exact width
|
// Remove any trailing spaces from dashLine before padding to exact width
|
||||||
const trimmedDashLine = dashLine.trimEnd();
|
const trimmedDashLine = dashLine.trimEnd();
|
||||||
// Pad the dashboard line to exactly halfWidth chars with no extra spaces
|
// Pad the dashboard line to exactly halfWidth chars with no extra spaces
|
||||||
const paddedDashLine = trimmedDashLine.padEnd(halfWidth, " ");
|
const paddedDashLine = trimmedDashLine.padEnd(halfWidth, ' ');
|
||||||
|
|
||||||
// Join the lines with no space in between
|
// Join the lines with no space in between
|
||||||
combinedLines.push(paddedDashLine + depLine);
|
combinedLines.push(paddedDashLine + depLine);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Join all lines and output
|
// Join all lines and output
|
||||||
console.log(combinedLines.join("\n"));
|
console.log(combinedLines.join('\n'));
|
||||||
} else {
|
} else {
|
||||||
// Terminal too narrow, show boxes stacked vertically
|
// Terminal too narrow, show boxes stacked vertically
|
||||||
const dashboardBox = boxen(projectDashboardContent, {
|
const dashboardBox = boxen(projectDashboardContent, {
|
||||||
padding: 1,
|
padding: 1,
|
||||||
borderColor: "blue",
|
borderColor: 'blue',
|
||||||
borderStyle: "round",
|
borderStyle: 'round',
|
||||||
margin: { top: 0, bottom: 1 },
|
margin: { top: 0, bottom: 1 }
|
||||||
});
|
});
|
||||||
|
|
||||||
const dependencyBox = boxen(dependencyDashboardContent, {
|
const dependencyBox = boxen(dependencyDashboardContent, {
|
||||||
padding: 1,
|
padding: 1,
|
||||||
borderColor: "magenta",
|
borderColor: 'magenta',
|
||||||
borderStyle: "round",
|
borderStyle: 'round',
|
||||||
margin: { top: 0, bottom: 1 },
|
margin: { top: 0, bottom: 1 }
|
||||||
});
|
});
|
||||||
|
|
||||||
// Display stacked vertically
|
// Display stacked vertically
|
||||||
@@ -403,8 +403,8 @@ function listTasks(
|
|||||||
boxen(
|
boxen(
|
||||||
statusFilter
|
statusFilter
|
||||||
? chalk.yellow(`No tasks with status '${statusFilter}' found`)
|
? chalk.yellow(`No tasks with status '${statusFilter}' found`)
|
||||||
: chalk.yellow("No tasks found"),
|
: chalk.yellow('No tasks found'),
|
||||||
{ padding: 1, borderColor: "yellow", borderStyle: "round" }
|
{ padding: 1, borderColor: 'yellow', borderStyle: 'round' }
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@@ -453,12 +453,12 @@ function listTasks(
|
|||||||
// Create a table with correct borders and spacing
|
// Create a table with correct borders and spacing
|
||||||
const table = new Table({
|
const table = new Table({
|
||||||
head: [
|
head: [
|
||||||
chalk.cyan.bold("ID"),
|
chalk.cyan.bold('ID'),
|
||||||
chalk.cyan.bold("Title"),
|
chalk.cyan.bold('Title'),
|
||||||
chalk.cyan.bold("Status"),
|
chalk.cyan.bold('Status'),
|
||||||
chalk.cyan.bold("Priority"),
|
chalk.cyan.bold('Priority'),
|
||||||
chalk.cyan.bold("Dependencies"),
|
chalk.cyan.bold('Dependencies'),
|
||||||
chalk.cyan.bold("Complexity"),
|
chalk.cyan.bold('Complexity')
|
||||||
],
|
],
|
||||||
colWidths: [
|
colWidths: [
|
||||||
idWidth,
|
idWidth,
|
||||||
@@ -466,21 +466,21 @@ function listTasks(
|
|||||||
statusWidth,
|
statusWidth,
|
||||||
priorityWidth,
|
priorityWidth,
|
||||||
depsWidth,
|
depsWidth,
|
||||||
complexityWidth, // Added complexity column width
|
complexityWidth // Added complexity column width
|
||||||
],
|
],
|
||||||
style: {
|
style: {
|
||||||
head: [], // No special styling for header
|
head: [], // No special styling for header
|
||||||
border: [], // No special styling for border
|
border: [], // No special styling for border
|
||||||
compact: false, // Use default spacing
|
compact: false // Use default spacing
|
||||||
},
|
},
|
||||||
wordWrap: true,
|
wordWrap: true,
|
||||||
wrapOnWordBoundary: true,
|
wrapOnWordBoundary: true
|
||||||
});
|
});
|
||||||
|
|
||||||
// Process tasks for the table
|
// Process tasks for the table
|
||||||
filteredTasks.forEach((task) => {
|
filteredTasks.forEach((task) => {
|
||||||
// Format dependencies with status indicators (colored)
|
// Format dependencies with status indicators (colored)
|
||||||
let depText = "None";
|
let depText = 'None';
|
||||||
if (task.dependencies && task.dependencies.length > 0) {
|
if (task.dependencies && task.dependencies.length > 0) {
|
||||||
// Use the proper formatDependenciesWithStatus function for colored status
|
// Use the proper formatDependenciesWithStatus function for colored status
|
||||||
depText = formatDependenciesWithStatus(
|
depText = formatDependenciesWithStatus(
|
||||||
@@ -490,19 +490,19 @@ function listTasks(
|
|||||||
complexityReport
|
complexityReport
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
depText = chalk.gray("None");
|
depText = chalk.gray('None');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up any ANSI codes or confusing characters
|
// Clean up any ANSI codes or confusing characters
|
||||||
const cleanTitle = task.title.replace(/\n/g, " ");
|
const cleanTitle = task.title.replace(/\n/g, ' ');
|
||||||
|
|
||||||
// Get priority color
|
// Get priority color
|
||||||
const priorityColor =
|
const priorityColor =
|
||||||
{
|
{
|
||||||
high: chalk.red,
|
high: chalk.red,
|
||||||
medium: chalk.yellow,
|
medium: chalk.yellow,
|
||||||
low: chalk.gray,
|
low: chalk.gray
|
||||||
}[task.priority || "medium"] || chalk.white;
|
}[task.priority || 'medium'] || chalk.white;
|
||||||
|
|
||||||
// Format status
|
// Format status
|
||||||
const status = getStatusWithColor(task.status, true);
|
const status = getStatusWithColor(task.status, true);
|
||||||
@@ -512,38 +512,38 @@ function listTasks(
|
|||||||
task.id.toString(),
|
task.id.toString(),
|
||||||
truncate(cleanTitle, titleWidth - 3),
|
truncate(cleanTitle, titleWidth - 3),
|
||||||
status,
|
status,
|
||||||
priorityColor(truncate(task.priority || "medium", priorityWidth - 2)),
|
priorityColor(truncate(task.priority || 'medium', priorityWidth - 2)),
|
||||||
depText,
|
depText,
|
||||||
task.complexityScore
|
task.complexityScore
|
||||||
? getComplexityWithColor(task.complexityScore)
|
? getComplexityWithColor(task.complexityScore)
|
||||||
: chalk.gray("N/A"),
|
: chalk.gray('N/A')
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Add subtasks if requested
|
// Add subtasks if requested
|
||||||
if (withSubtasks && task.subtasks && task.subtasks.length > 0) {
|
if (withSubtasks && task.subtasks && task.subtasks.length > 0) {
|
||||||
task.subtasks.forEach((subtask) => {
|
task.subtasks.forEach((subtask) => {
|
||||||
// Format subtask dependencies with status indicators
|
// Format subtask dependencies with status indicators
|
||||||
let subtaskDepText = "None";
|
let subtaskDepText = 'None';
|
||||||
if (subtask.dependencies && subtask.dependencies.length > 0) {
|
if (subtask.dependencies && subtask.dependencies.length > 0) {
|
||||||
// Handle both subtask-to-subtask and subtask-to-task dependencies
|
// Handle both subtask-to-subtask and subtask-to-task dependencies
|
||||||
const formattedDeps = subtask.dependencies
|
const formattedDeps = subtask.dependencies
|
||||||
.map((depId) => {
|
.map((depId) => {
|
||||||
// Check if it's a dependency on another subtask
|
// Check if it's a dependency on another subtask
|
||||||
if (typeof depId === "number" && depId < 100) {
|
if (typeof depId === 'number' && depId < 100) {
|
||||||
const foundSubtask = task.subtasks.find(
|
const foundSubtask = task.subtasks.find(
|
||||||
(st) => st.id === depId
|
(st) => st.id === depId
|
||||||
);
|
);
|
||||||
if (foundSubtask) {
|
if (foundSubtask) {
|
||||||
const isDone =
|
const isDone =
|
||||||
foundSubtask.status === "done" ||
|
foundSubtask.status === 'done' ||
|
||||||
foundSubtask.status === "completed";
|
foundSubtask.status === 'completed';
|
||||||
const isInProgress = foundSubtask.status === "in-progress";
|
const isInProgress = foundSubtask.status === 'in-progress';
|
||||||
|
|
||||||
// Use consistent color formatting instead of emojis
|
// Use consistent color formatting instead of emojis
|
||||||
if (isDone) {
|
if (isDone) {
|
||||||
return chalk.green.bold(`${task.id}.${depId}`);
|
return chalk.green.bold(`${task.id}.${depId}`);
|
||||||
} else if (isInProgress) {
|
} else if (isInProgress) {
|
||||||
return chalk.hex("#FFA500").bold(`${task.id}.${depId}`);
|
return chalk.hex('#FFA500').bold(`${task.id}.${depId}`);
|
||||||
} else {
|
} else {
|
||||||
return chalk.red.bold(`${task.id}.${depId}`);
|
return chalk.red.bold(`${task.id}.${depId}`);
|
||||||
}
|
}
|
||||||
@@ -555,22 +555,22 @@ function listTasks(
|
|||||||
// Add complexity to depTask before checking status
|
// Add complexity to depTask before checking status
|
||||||
addComplexityToTask(depTask, complexityReport);
|
addComplexityToTask(depTask, complexityReport);
|
||||||
const isDone =
|
const isDone =
|
||||||
depTask.status === "done" || depTask.status === "completed";
|
depTask.status === 'done' || depTask.status === 'completed';
|
||||||
const isInProgress = depTask.status === "in-progress";
|
const isInProgress = depTask.status === 'in-progress';
|
||||||
// Use the same color scheme as in formatDependenciesWithStatus
|
// Use the same color scheme as in formatDependenciesWithStatus
|
||||||
if (isDone) {
|
if (isDone) {
|
||||||
return chalk.green.bold(`${depId}`);
|
return chalk.green.bold(`${depId}`);
|
||||||
} else if (isInProgress) {
|
} else if (isInProgress) {
|
||||||
return chalk.hex("#FFA500").bold(`${depId}`);
|
return chalk.hex('#FFA500').bold(`${depId}`);
|
||||||
} else {
|
} else {
|
||||||
return chalk.red.bold(`${depId}`);
|
return chalk.red.bold(`${depId}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return chalk.cyan(depId.toString());
|
return chalk.cyan(depId.toString());
|
||||||
})
|
})
|
||||||
.join(", ");
|
.join(', ');
|
||||||
|
|
||||||
subtaskDepText = formattedDeps || chalk.gray("None");
|
subtaskDepText = formattedDeps || chalk.gray('None');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the subtask row without truncating dependencies
|
// Add the subtask row without truncating dependencies
|
||||||
@@ -578,11 +578,11 @@ function listTasks(
|
|||||||
`${task.id}.${subtask.id}`,
|
`${task.id}.${subtask.id}`,
|
||||||
chalk.dim(`└─ ${truncate(subtask.title, titleWidth - 5)}`),
|
chalk.dim(`└─ ${truncate(subtask.title, titleWidth - 5)}`),
|
||||||
getStatusWithColor(subtask.status, true),
|
getStatusWithColor(subtask.status, true),
|
||||||
chalk.dim("-"),
|
chalk.dim('-'),
|
||||||
subtaskDepText,
|
subtaskDepText,
|
||||||
subtask.complexityScore
|
subtask.complexityScore
|
||||||
? chalk.gray(`${subtask.complexityScore}`)
|
? chalk.gray(`${subtask.complexityScore}`)
|
||||||
: chalk.gray("N/A"),
|
: chalk.gray('N/A')
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -592,12 +592,12 @@ function listTasks(
|
|||||||
try {
|
try {
|
||||||
console.log(table.toString());
|
console.log(table.toString());
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log("error", `Error rendering table: ${err.message}`);
|
log('error', `Error rendering table: ${err.message}`);
|
||||||
|
|
||||||
// Fall back to simpler output
|
// Fall back to simpler output
|
||||||
console.log(
|
console.log(
|
||||||
chalk.yellow(
|
chalk.yellow(
|
||||||
"\nFalling back to simple task list due to terminal width constraints:"
|
'\nFalling back to simple task list due to terminal width constraints:'
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
filteredTasks.forEach((task) => {
|
filteredTasks.forEach((task) => {
|
||||||
@@ -619,13 +619,13 @@ function listTasks(
|
|||||||
const priorityColors = {
|
const priorityColors = {
|
||||||
high: chalk.red.bold,
|
high: chalk.red.bold,
|
||||||
medium: chalk.yellow,
|
medium: chalk.yellow,
|
||||||
low: chalk.gray,
|
low: chalk.gray
|
||||||
};
|
};
|
||||||
|
|
||||||
// Show next task box in a prominent color
|
// Show next task box in a prominent color
|
||||||
if (nextItem) {
|
if (nextItem) {
|
||||||
// Prepare subtasks section if they exist (Only tasks have .subtasks property)
|
// Prepare subtasks section if they exist (Only tasks have .subtasks property)
|
||||||
let subtasksSection = "";
|
let subtasksSection = '';
|
||||||
// Check if the nextItem is a top-level task before looking for subtasks
|
// Check if the nextItem is a top-level task before looking for subtasks
|
||||||
const parentTaskForSubtasks = data.tasks.find(
|
const parentTaskForSubtasks = data.tasks.find(
|
||||||
(t) => String(t.id) === String(nextItem.id)
|
(t) => String(t.id) === String(nextItem.id)
|
||||||
@@ -635,75 +635,75 @@ function listTasks(
|
|||||||
parentTaskForSubtasks.subtasks &&
|
parentTaskForSubtasks.subtasks &&
|
||||||
parentTaskForSubtasks.subtasks.length > 0
|
parentTaskForSubtasks.subtasks.length > 0
|
||||||
) {
|
) {
|
||||||
subtasksSection = `\n\n${chalk.white.bold("Subtasks:")}\n`;
|
subtasksSection = `\n\n${chalk.white.bold('Subtasks:')}\n`;
|
||||||
subtasksSection += parentTaskForSubtasks.subtasks
|
subtasksSection += parentTaskForSubtasks.subtasks
|
||||||
.map((subtask) => {
|
.map((subtask) => {
|
||||||
// Add complexity to subtask before display
|
// Add complexity to subtask before display
|
||||||
addComplexityToTask(subtask, complexityReport);
|
addComplexityToTask(subtask, complexityReport);
|
||||||
// Using a more simplified format for subtask status display
|
// Using a more simplified format for subtask status display
|
||||||
const status = subtask.status || "pending";
|
const status = subtask.status || 'pending';
|
||||||
const statusColors = {
|
const statusColors = {
|
||||||
done: chalk.green,
|
done: chalk.green,
|
||||||
completed: chalk.green,
|
completed: chalk.green,
|
||||||
pending: chalk.yellow,
|
pending: chalk.yellow,
|
||||||
"in-progress": chalk.blue,
|
'in-progress': chalk.blue,
|
||||||
deferred: chalk.gray,
|
deferred: chalk.gray,
|
||||||
blocked: chalk.red,
|
blocked: chalk.red,
|
||||||
cancelled: chalk.gray,
|
cancelled: chalk.gray
|
||||||
};
|
};
|
||||||
const statusColor =
|
const statusColor =
|
||||||
statusColors[status.toLowerCase()] || chalk.white;
|
statusColors[status.toLowerCase()] || chalk.white;
|
||||||
// Ensure subtask ID is displayed correctly using parent ID from the original task object
|
// Ensure subtask ID is displayed correctly using parent ID from the original task object
|
||||||
return `${chalk.cyan(`${parentTaskForSubtasks.id}.${subtask.id}`)} [${statusColor(status)}] ${subtask.title}`;
|
return `${chalk.cyan(`${parentTaskForSubtasks.id}.${subtask.id}`)} [${statusColor(status)}] ${subtask.title}`;
|
||||||
})
|
})
|
||||||
.join("\n");
|
.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
boxen(
|
boxen(
|
||||||
chalk.hex("#FF8800").bold(
|
chalk.hex('#FF8800').bold(
|
||||||
// Use nextItem.id and nextItem.title
|
// Use nextItem.id and nextItem.title
|
||||||
`🔥 Next Task to Work On: #${nextItem.id} - ${nextItem.title}`
|
`🔥 Next Task to Work On: #${nextItem.id} - ${nextItem.title}`
|
||||||
) +
|
) +
|
||||||
"\n\n" +
|
'\n\n' +
|
||||||
// Use nextItem.priority, nextItem.status, nextItem.dependencies
|
// Use nextItem.priority, nextItem.status, nextItem.dependencies
|
||||||
`${chalk.white("Priority:")} ${priorityColors[nextItem.priority || "medium"](nextItem.priority || "medium")} ${chalk.white("Status:")} ${getStatusWithColor(nextItem.status, true)}\n` +
|
`${chalk.white('Priority:')} ${priorityColors[nextItem.priority || 'medium'](nextItem.priority || 'medium')} ${chalk.white('Status:')} ${getStatusWithColor(nextItem.status, true)}\n` +
|
||||||
`${chalk.white("Dependencies:")} ${nextItem.dependencies && nextItem.dependencies.length > 0 ? formatDependenciesWithStatus(nextItem.dependencies, data.tasks, true, complexityReport) : chalk.gray("None")}\n\n` +
|
`${chalk.white('Dependencies:')} ${nextItem.dependencies && nextItem.dependencies.length > 0 ? formatDependenciesWithStatus(nextItem.dependencies, data.tasks, true, complexityReport) : chalk.gray('None')}\n\n` +
|
||||||
// Use nextTask.description (Note: findNextTask doesn't return description, need to fetch original task/subtask for this)
|
// Use nextTask.description (Note: findNextTask doesn't return description, need to fetch original task/subtask for this)
|
||||||
// *** Fetching original item for description and details ***
|
// *** Fetching original item for description and details ***
|
||||||
`${chalk.white("Description:")} ${getWorkItemDescription(nextItem, data.tasks)}` +
|
`${chalk.white('Description:')} ${getWorkItemDescription(nextItem, data.tasks)}` +
|
||||||
subtasksSection + // <-- Subtasks are handled above now
|
subtasksSection + // <-- Subtasks are handled above now
|
||||||
"\n\n" +
|
'\n\n' +
|
||||||
// Use nextItem.id
|
// Use nextItem.id
|
||||||
`${chalk.cyan("Start working:")} ${chalk.yellow(`task-master set-status --id=${nextItem.id} --status=in-progress`)}\n` +
|
`${chalk.cyan('Start working:')} ${chalk.yellow(`task-master set-status --id=${nextItem.id} --status=in-progress`)}\n` +
|
||||||
// Use nextItem.id
|
// Use nextItem.id
|
||||||
`${chalk.cyan("View details:")} ${chalk.yellow(`task-master show ${nextItem.id}`)}`,
|
`${chalk.cyan('View details:')} ${chalk.yellow(`task-master show ${nextItem.id}`)}`,
|
||||||
{
|
{
|
||||||
padding: { left: 2, right: 2, top: 1, bottom: 1 },
|
padding: { left: 2, right: 2, top: 1, bottom: 1 },
|
||||||
borderColor: "#FF8800",
|
borderColor: '#FF8800',
|
||||||
borderStyle: "round",
|
borderStyle: 'round',
|
||||||
margin: { top: 1, bottom: 1 },
|
margin: { top: 1, bottom: 1 },
|
||||||
title: "⚡ RECOMMENDED NEXT TASK ⚡",
|
title: '⚡ RECOMMENDED NEXT TASK ⚡',
|
||||||
titleAlignment: "center",
|
titleAlignment: 'center',
|
||||||
width: terminalWidth - 4,
|
width: terminalWidth - 4,
|
||||||
fullscreen: false,
|
fullscreen: false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
console.log(
|
||||||
boxen(
|
boxen(
|
||||||
chalk.hex("#FF8800").bold("No eligible next task found") +
|
chalk.hex('#FF8800').bold('No eligible next task found') +
|
||||||
"\n\n" +
|
'\n\n' +
|
||||||
"All pending tasks have dependencies that are not yet completed, or all tasks are done.",
|
'All pending tasks have dependencies that are not yet completed, or all tasks are done.',
|
||||||
{
|
{
|
||||||
padding: 1,
|
padding: 1,
|
||||||
borderColor: "#FF8800",
|
borderColor: '#FF8800',
|
||||||
borderStyle: "round",
|
borderStyle: 'round',
|
||||||
margin: { top: 1, bottom: 1 },
|
margin: { top: 1, bottom: 1 },
|
||||||
title: "⚡ NEXT TASK ⚡",
|
title: '⚡ NEXT TASK ⚡',
|
||||||
titleAlignment: "center",
|
titleAlignment: 'center',
|
||||||
width: terminalWidth - 4, // Use full terminal width minus a small margin
|
width: terminalWidth - 4 // Use full terminal width minus a small margin
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -712,28 +712,28 @@ function listTasks(
|
|||||||
// Show next steps
|
// Show next steps
|
||||||
console.log(
|
console.log(
|
||||||
boxen(
|
boxen(
|
||||||
chalk.white.bold("Suggested Next Steps:") +
|
chalk.white.bold('Suggested Next Steps:') +
|
||||||
"\n\n" +
|
'\n\n' +
|
||||||
`${chalk.cyan("1.")} Run ${chalk.yellow("task-master next")} to see what to work on next\n` +
|
`${chalk.cyan('1.')} Run ${chalk.yellow('task-master next')} to see what to work on next\n` +
|
||||||
`${chalk.cyan("2.")} Run ${chalk.yellow("task-master expand --id=<id>")} to break down a task into subtasks\n` +
|
`${chalk.cyan('2.')} Run ${chalk.yellow('task-master expand --id=<id>')} to break down a task into subtasks\n` +
|
||||||
`${chalk.cyan("3.")} Run ${chalk.yellow("task-master set-status --id=<id> --status=done")} to mark a task as complete`,
|
`${chalk.cyan('3.')} Run ${chalk.yellow('task-master set-status --id=<id> --status=done')} to mark a task as complete`,
|
||||||
{
|
{
|
||||||
padding: 1,
|
padding: 1,
|
||||||
borderColor: "gray",
|
borderColor: 'gray',
|
||||||
borderStyle: "round",
|
borderStyle: 'round',
|
||||||
margin: { top: 1 },
|
margin: { top: 1 }
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log("error", `Error listing tasks: ${error.message}`);
|
log('error', `Error listing tasks: ${error.message}`);
|
||||||
|
|
||||||
if (outputFormat === "json") {
|
if (outputFormat === 'json') {
|
||||||
// Return structured error for JSON output
|
// Return structured error for JSON output
|
||||||
throw {
|
throw {
|
||||||
code: "TASK_LIST_ERROR",
|
code: 'TASK_LIST_ERROR',
|
||||||
message: error.message,
|
message: error.message,
|
||||||
details: error.stack,
|
details: error.stack
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -744,18 +744,18 @@ function listTasks(
|
|||||||
|
|
||||||
// *** Helper function to get description for task or subtask ***
|
// *** Helper function to get description for task or subtask ***
|
||||||
function getWorkItemDescription(item, allTasks) {
|
function getWorkItemDescription(item, allTasks) {
|
||||||
if (!item) return "N/A";
|
if (!item) return 'N/A';
|
||||||
if (item.parentId) {
|
if (item.parentId) {
|
||||||
// It's a subtask
|
// It's a subtask
|
||||||
const parent = allTasks.find((t) => t.id === item.parentId);
|
const parent = allTasks.find((t) => t.id === item.parentId);
|
||||||
const subtask = parent?.subtasks?.find(
|
const subtask = parent?.subtasks?.find(
|
||||||
(st) => `${parent.id}.${st.id}` === item.id
|
(st) => `${parent.id}.${st.id}` === item.id
|
||||||
);
|
);
|
||||||
return subtask?.description || "No description available.";
|
return subtask?.description || 'No description available.';
|
||||||
} else {
|
} else {
|
||||||
// It's a top-level task
|
// It's a top-level task
|
||||||
const task = allTasks.find((t) => String(t.id) === String(item.id));
|
const task = allTasks.find((t) => String(t.id) === String(item.id));
|
||||||
return task?.description || "No description available.";
|
return task?.description || 'No description available.';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
* Core functionality for managing AI model configurations
|
* Core functionality for managing AI model configurations
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import https from "https";
|
import https from 'https';
|
||||||
import http from "http";
|
import http from 'http';
|
||||||
import {
|
import {
|
||||||
getMainModelId,
|
getMainModelId,
|
||||||
getResearchModelId,
|
getResearchModelId,
|
||||||
@@ -19,10 +19,10 @@ import {
|
|||||||
writeConfig,
|
writeConfig,
|
||||||
isConfigFilePresent,
|
isConfigFilePresent,
|
||||||
getAllProviders,
|
getAllProviders,
|
||||||
getBaseUrlForRole,
|
getBaseUrlForRole
|
||||||
} from "../config-manager.js";
|
} from '../config-manager.js';
|
||||||
import { findConfigPath } from "../../../src/utils/path-utils.js";
|
import { findConfigPath } from '../../../src/utils/path-utils.js';
|
||||||
import { log } from "../utils.js";
|
import { log } from '../utils.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches the list of models from OpenRouter API.
|
* Fetches the list of models from OpenRouter API.
|
||||||
@@ -31,26 +31,26 @@ import { log } from "../utils.js";
|
|||||||
function fetchOpenRouterModels() {
|
function fetchOpenRouterModels() {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const options = {
|
const options = {
|
||||||
hostname: "openrouter.ai",
|
hostname: 'openrouter.ai',
|
||||||
path: "/api/v1/models",
|
path: '/api/v1/models',
|
||||||
method: "GET",
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: "application/json",
|
Accept: 'application/json'
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const req = https.request(options, (res) => {
|
const req = https.request(options, (res) => {
|
||||||
let data = "";
|
let data = '';
|
||||||
res.on("data", (chunk) => {
|
res.on('data', (chunk) => {
|
||||||
data += chunk;
|
data += chunk;
|
||||||
});
|
});
|
||||||
res.on("end", () => {
|
res.on('end', () => {
|
||||||
if (res.statusCode === 200) {
|
if (res.statusCode === 200) {
|
||||||
try {
|
try {
|
||||||
const parsedData = JSON.parse(data);
|
const parsedData = JSON.parse(data);
|
||||||
resolve(parsedData.data || []); // Return the array of models
|
resolve(parsedData.data || []); // Return the array of models
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error parsing OpenRouter response:", e);
|
console.error('Error parsing OpenRouter response:', e);
|
||||||
resolve(null); // Indicate failure
|
resolve(null); // Indicate failure
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -62,8 +62,8 @@ function fetchOpenRouterModels() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
req.on("error", (e) => {
|
req.on('error', (e) => {
|
||||||
console.error("Error fetching OpenRouter models:", e);
|
console.error('Error fetching OpenRouter models:', e);
|
||||||
resolve(null); // Indicate failure
|
resolve(null); // Indicate failure
|
||||||
});
|
});
|
||||||
req.end();
|
req.end();
|
||||||
@@ -75,14 +75,14 @@ function fetchOpenRouterModels() {
|
|||||||
* @param {string} baseURL - The base URL for the Ollama API (e.g., "http://localhost:11434/api")
|
* @param {string} baseURL - The base URL for the Ollama API (e.g., "http://localhost:11434/api")
|
||||||
* @returns {Promise<Array|null>} A promise that resolves with the list of model objects or null if fetch fails.
|
* @returns {Promise<Array|null>} A promise that resolves with the list of model objects or null if fetch fails.
|
||||||
*/
|
*/
|
||||||
function fetchOllamaModels(baseURL = "http://localhost:11434/api") {
|
function fetchOllamaModels(baseURL = 'http://localhost:11434/api') {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
try {
|
try {
|
||||||
// Parse the base URL to extract hostname, port, and base path
|
// Parse the base URL to extract hostname, port, and base path
|
||||||
const url = new URL(baseURL);
|
const url = new URL(baseURL);
|
||||||
const isHttps = url.protocol === "https:";
|
const isHttps = url.protocol === 'https:';
|
||||||
const port = url.port || (isHttps ? 443 : 80);
|
const port = url.port || (isHttps ? 443 : 80);
|
||||||
const basePath = url.pathname.endsWith("/")
|
const basePath = url.pathname.endsWith('/')
|
||||||
? url.pathname.slice(0, -1)
|
? url.pathname.slice(0, -1)
|
||||||
: url.pathname;
|
: url.pathname;
|
||||||
|
|
||||||
@@ -90,25 +90,25 @@ function fetchOllamaModels(baseURL = "http://localhost:11434/api") {
|
|||||||
hostname: url.hostname,
|
hostname: url.hostname,
|
||||||
port: parseInt(port, 10),
|
port: parseInt(port, 10),
|
||||||
path: `${basePath}/tags`,
|
path: `${basePath}/tags`,
|
||||||
method: "GET",
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: "application/json",
|
Accept: 'application/json'
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const requestLib = isHttps ? https : http;
|
const requestLib = isHttps ? https : http;
|
||||||
const req = requestLib.request(options, (res) => {
|
const req = requestLib.request(options, (res) => {
|
||||||
let data = "";
|
let data = '';
|
||||||
res.on("data", (chunk) => {
|
res.on('data', (chunk) => {
|
||||||
data += chunk;
|
data += chunk;
|
||||||
});
|
});
|
||||||
res.on("end", () => {
|
res.on('end', () => {
|
||||||
if (res.statusCode === 200) {
|
if (res.statusCode === 200) {
|
||||||
try {
|
try {
|
||||||
const parsedData = JSON.parse(data);
|
const parsedData = JSON.parse(data);
|
||||||
resolve(parsedData.models || []); // Return the array of models
|
resolve(parsedData.models || []); // Return the array of models
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error parsing Ollama response:", e);
|
console.error('Error parsing Ollama response:', e);
|
||||||
resolve(null); // Indicate failure
|
resolve(null); // Indicate failure
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -120,13 +120,13 @@ function fetchOllamaModels(baseURL = "http://localhost:11434/api") {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
req.on("error", (e) => {
|
req.on('error', (e) => {
|
||||||
console.error("Error fetching Ollama models:", e);
|
console.error('Error fetching Ollama models:', e);
|
||||||
resolve(null); // Indicate failure
|
resolve(null); // Indicate failure
|
||||||
});
|
});
|
||||||
req.end();
|
req.end();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error parsing Ollama base URL:", e);
|
console.error('Error parsing Ollama base URL:', e);
|
||||||
resolve(null); // Indicate failure
|
resolve(null); // Indicate failure
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -144,13 +144,13 @@ async function getModelConfiguration(options = {}) {
|
|||||||
const { mcpLog, projectRoot, session } = options;
|
const { mcpLog, projectRoot, session } = options;
|
||||||
|
|
||||||
const report = (level, ...args) => {
|
const report = (level, ...args) => {
|
||||||
if (mcpLog && typeof mcpLog[level] === "function") {
|
if (mcpLog && typeof mcpLog[level] === 'function') {
|
||||||
mcpLog[level](...args);
|
mcpLog[level](...args);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!projectRoot) {
|
if (!projectRoot) {
|
||||||
throw new Error("Project root is required but not found.");
|
throw new Error('Project root is required but not found.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use centralized config path finding instead of hardcoded path
|
// Use centralized config path finding instead of hardcoded path
|
||||||
@@ -158,11 +158,11 @@ async function getModelConfiguration(options = {}) {
|
|||||||
const configExists = isConfigFilePresent(projectRoot);
|
const configExists = isConfigFilePresent(projectRoot);
|
||||||
|
|
||||||
log(
|
log(
|
||||||
"debug",
|
'debug',
|
||||||
`Checking for config file using findConfigPath, found: ${configPath}`
|
`Checking for config file using findConfigPath, found: ${configPath}`
|
||||||
);
|
);
|
||||||
log(
|
log(
|
||||||
"debug",
|
'debug',
|
||||||
`Checking config file using isConfigFilePresent(), exists: ${configExists}`
|
`Checking config file using isConfigFilePresent(), exists: ${configExists}`
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -221,8 +221,8 @@ async function getModelConfiguration(options = {}) {
|
|||||||
cost: mainModelData?.cost_per_1m_tokens || null,
|
cost: mainModelData?.cost_per_1m_tokens || null,
|
||||||
keyStatus: {
|
keyStatus: {
|
||||||
cli: mainCliKeyOk,
|
cli: mainCliKeyOk,
|
||||||
mcp: mainMcpKeyOk,
|
mcp: mainMcpKeyOk
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
research: {
|
research: {
|
||||||
provider: researchProvider,
|
provider: researchProvider,
|
||||||
@@ -231,8 +231,8 @@ async function getModelConfiguration(options = {}) {
|
|||||||
cost: researchModelData?.cost_per_1m_tokens || null,
|
cost: researchModelData?.cost_per_1m_tokens || null,
|
||||||
keyStatus: {
|
keyStatus: {
|
||||||
cli: researchCliKeyOk,
|
cli: researchCliKeyOk,
|
||||||
mcp: researchMcpKeyOk,
|
mcp: researchMcpKeyOk
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
fallback: fallbackProvider
|
fallback: fallbackProvider
|
||||||
? {
|
? {
|
||||||
@@ -242,22 +242,22 @@ async function getModelConfiguration(options = {}) {
|
|||||||
cost: fallbackModelData?.cost_per_1m_tokens || null,
|
cost: fallbackModelData?.cost_per_1m_tokens || null,
|
||||||
keyStatus: {
|
keyStatus: {
|
||||||
cli: fallbackCliKeyOk,
|
cli: fallbackCliKeyOk,
|
||||||
mcp: fallbackMcpKeyOk,
|
mcp: fallbackMcpKeyOk
|
||||||
},
|
|
||||||
}
|
}
|
||||||
: null,
|
}
|
||||||
},
|
: null
|
||||||
message: "Successfully retrieved current model configuration",
|
|
||||||
},
|
},
|
||||||
|
message: 'Successfully retrieved current model configuration'
|
||||||
|
}
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
report("error", `Error getting model configuration: ${error.message}`);
|
report('error', `Error getting model configuration: ${error.message}`);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: {
|
error: {
|
||||||
code: "CONFIG_ERROR",
|
code: 'CONFIG_ERROR',
|
||||||
message: error.message,
|
message: error.message
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -274,13 +274,13 @@ async function getAvailableModelsList(options = {}) {
|
|||||||
const { mcpLog, projectRoot } = options;
|
const { mcpLog, projectRoot } = options;
|
||||||
|
|
||||||
const report = (level, ...args) => {
|
const report = (level, ...args) => {
|
||||||
if (mcpLog && typeof mcpLog[level] === "function") {
|
if (mcpLog && typeof mcpLog[level] === 'function') {
|
||||||
mcpLog[level](...args);
|
mcpLog[level](...args);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!projectRoot) {
|
if (!projectRoot) {
|
||||||
throw new Error("Project root is required but not found.");
|
throw new Error('Project root is required but not found.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use centralized config path finding instead of hardcoded path
|
// Use centralized config path finding instead of hardcoded path
|
||||||
@@ -288,11 +288,11 @@ async function getAvailableModelsList(options = {}) {
|
|||||||
const configExists = isConfigFilePresent(projectRoot);
|
const configExists = isConfigFilePresent(projectRoot);
|
||||||
|
|
||||||
log(
|
log(
|
||||||
"debug",
|
'debug',
|
||||||
`Checking for config file using findConfigPath, found: ${configPath}`
|
`Checking for config file using findConfigPath, found: ${configPath}`
|
||||||
);
|
);
|
||||||
log(
|
log(
|
||||||
"debug",
|
'debug',
|
||||||
`Checking config file using isConfigFilePresent(), exists: ${configExists}`
|
`Checking config file using isConfigFilePresent(), exists: ${configExists}`
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -311,8 +311,8 @@ async function getAvailableModelsList(options = {}) {
|
|||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
models: [],
|
models: [],
|
||||||
message: "No available models found",
|
message: 'No available models found'
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,28 +326,28 @@ async function getAvailableModelsList(options = {}) {
|
|||||||
Boolean
|
Boolean
|
||||||
);
|
);
|
||||||
const otherAvailableModels = allAvailableModels.map((model) => ({
|
const otherAvailableModels = allAvailableModels.map((model) => ({
|
||||||
provider: model.provider || "N/A",
|
provider: model.provider || 'N/A',
|
||||||
modelId: model.id,
|
modelId: model.id,
|
||||||
sweScore: model.swe_score || null,
|
sweScore: model.swe_score || null,
|
||||||
cost: model.cost_per_1m_tokens || null,
|
cost: model.cost_per_1m_tokens || null,
|
||||||
allowedRoles: model.allowed_roles || [],
|
allowedRoles: model.allowed_roles || []
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
models: otherAvailableModels,
|
models: otherAvailableModels,
|
||||||
message: `Successfully retrieved ${otherAvailableModels.length} available models`,
|
message: `Successfully retrieved ${otherAvailableModels.length} available models`
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
report("error", `Error getting available models: ${error.message}`);
|
report('error', `Error getting available models: ${error.message}`);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: {
|
error: {
|
||||||
code: "MODELS_LIST_ERROR",
|
code: 'MODELS_LIST_ERROR',
|
||||||
message: error.message,
|
message: error.message
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -367,13 +367,13 @@ async function setModel(role, modelId, options = {}) {
|
|||||||
const { mcpLog, projectRoot, providerHint } = options;
|
const { mcpLog, projectRoot, providerHint } = options;
|
||||||
|
|
||||||
const report = (level, ...args) => {
|
const report = (level, ...args) => {
|
||||||
if (mcpLog && typeof mcpLog[level] === "function") {
|
if (mcpLog && typeof mcpLog[level] === 'function') {
|
||||||
mcpLog[level](...args);
|
mcpLog[level](...args);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!projectRoot) {
|
if (!projectRoot) {
|
||||||
throw new Error("Project root is required but not found.");
|
throw new Error('Project root is required but not found.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use centralized config path finding instead of hardcoded path
|
// Use centralized config path finding instead of hardcoded path
|
||||||
@@ -381,11 +381,11 @@ async function setModel(role, modelId, options = {}) {
|
|||||||
const configExists = isConfigFilePresent(projectRoot);
|
const configExists = isConfigFilePresent(projectRoot);
|
||||||
|
|
||||||
log(
|
log(
|
||||||
"debug",
|
'debug',
|
||||||
`Checking for config file using findConfigPath, found: ${configPath}`
|
`Checking for config file using findConfigPath, found: ${configPath}`
|
||||||
);
|
);
|
||||||
log(
|
log(
|
||||||
"debug",
|
'debug',
|
||||||
`Checking config file using isConfigFilePresent(), exists: ${configExists}`
|
`Checking config file using isConfigFilePresent(), exists: ${configExists}`
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -396,24 +396,24 @@ async function setModel(role, modelId, options = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate role
|
// Validate role
|
||||||
if (!["main", "research", "fallback"].includes(role)) {
|
if (!['main', 'research', 'fallback'].includes(role)) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: {
|
error: {
|
||||||
code: "INVALID_ROLE",
|
code: 'INVALID_ROLE',
|
||||||
message: `Invalid role: ${role}. Must be one of: main, research, fallback.`,
|
message: `Invalid role: ${role}. Must be one of: main, research, fallback.`
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate model ID
|
// Validate model ID
|
||||||
if (typeof modelId !== "string" || modelId.trim() === "") {
|
if (typeof modelId !== 'string' || modelId.trim() === '') {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: {
|
error: {
|
||||||
code: "INVALID_MODEL_ID",
|
code: 'INVALID_MODEL_ID',
|
||||||
message: `Invalid model ID: ${modelId}. Must be a non-empty string.`,
|
message: `Invalid model ID: ${modelId}. Must be a non-empty string.`
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -434,40 +434,40 @@ async function setModel(role, modelId, options = {}) {
|
|||||||
// Found internally AND provider matches the hint
|
// Found internally AND provider matches the hint
|
||||||
determinedProvider = providerHint;
|
determinedProvider = providerHint;
|
||||||
report(
|
report(
|
||||||
"info",
|
'info',
|
||||||
`Model ${modelId} found internally with matching provider hint ${determinedProvider}.`
|
`Model ${modelId} found internally with matching provider hint ${determinedProvider}.`
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Either not found internally, OR found but under a DIFFERENT provider than hinted.
|
// Either not found internally, OR found but under a DIFFERENT provider than hinted.
|
||||||
// Proceed with custom logic based ONLY on the hint.
|
// Proceed with custom logic based ONLY on the hint.
|
||||||
if (providerHint === "openrouter") {
|
if (providerHint === 'openrouter') {
|
||||||
// Check OpenRouter ONLY because hint was openrouter
|
// Check OpenRouter ONLY because hint was openrouter
|
||||||
report("info", `Checking OpenRouter for ${modelId} (as hinted)...`);
|
report('info', `Checking OpenRouter for ${modelId} (as hinted)...`);
|
||||||
const openRouterModels = await fetchOpenRouterModels();
|
const openRouterModels = await fetchOpenRouterModels();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
openRouterModels &&
|
openRouterModels &&
|
||||||
openRouterModels.some((m) => m.id === modelId)
|
openRouterModels.some((m) => m.id === modelId)
|
||||||
) {
|
) {
|
||||||
determinedProvider = "openrouter";
|
determinedProvider = 'openrouter';
|
||||||
|
|
||||||
// Check if this is a free model (ends with :free)
|
// Check if this is a free model (ends with :free)
|
||||||
if (modelId.endsWith(":free")) {
|
if (modelId.endsWith(':free')) {
|
||||||
warningMessage = `Warning: OpenRouter free model '${modelId}' selected. Free models have significant limitations including lower context windows, reduced rate limits, and may not support advanced features like tool_use. Consider using the paid version '${modelId.replace(":free", "")}' for full functionality.`;
|
warningMessage = `Warning: OpenRouter free model '${modelId}' selected. Free models have significant limitations including lower context windows, reduced rate limits, and may not support advanced features like tool_use. Consider using the paid version '${modelId.replace(':free', '')}' for full functionality.`;
|
||||||
} else {
|
} else {
|
||||||
warningMessage = `Warning: Custom OpenRouter model '${modelId}' set. This model is not officially validated by Taskmaster and may not function as expected.`;
|
warningMessage = `Warning: Custom OpenRouter model '${modelId}' set. This model is not officially validated by Taskmaster and may not function as expected.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
report("warn", warningMessage);
|
report('warn', warningMessage);
|
||||||
} else {
|
} else {
|
||||||
// Hinted as OpenRouter but not found in live check
|
// Hinted as OpenRouter but not found in live check
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Model ID "${modelId}" not found in the live OpenRouter model list. Please verify the ID and ensure it's available on OpenRouter.`
|
`Model ID "${modelId}" not found in the live OpenRouter model list. Please verify the ID and ensure it's available on OpenRouter.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (providerHint === "ollama") {
|
} else if (providerHint === 'ollama') {
|
||||||
// Check Ollama ONLY because hint was ollama
|
// Check Ollama ONLY because hint was ollama
|
||||||
report("info", `Checking Ollama for ${modelId} (as hinted)...`);
|
report('info', `Checking Ollama for ${modelId} (as hinted)...`);
|
||||||
|
|
||||||
// Get the Ollama base URL from config
|
// Get the Ollama base URL from config
|
||||||
const ollamaBaseURL = getBaseUrlForRole(role, projectRoot);
|
const ollamaBaseURL = getBaseUrlForRole(role, projectRoot);
|
||||||
@@ -479,9 +479,9 @@ async function setModel(role, modelId, options = {}) {
|
|||||||
`Unable to connect to Ollama server at ${ollamaBaseURL}. Please ensure Ollama is running and try again.`
|
`Unable to connect to Ollama server at ${ollamaBaseURL}. Please ensure Ollama is running and try again.`
|
||||||
);
|
);
|
||||||
} else if (ollamaModels.some((m) => m.model === modelId)) {
|
} else if (ollamaModels.some((m) => m.model === modelId)) {
|
||||||
determinedProvider = "ollama";
|
determinedProvider = 'ollama';
|
||||||
warningMessage = `Warning: Custom Ollama model '${modelId}' set. Ensure your Ollama server is running and has pulled this model. Taskmaster cannot guarantee compatibility.`;
|
warningMessage = `Warning: Custom Ollama model '${modelId}' set. Ensure your Ollama server is running and has pulled this model. Taskmaster cannot guarantee compatibility.`;
|
||||||
report("warn", warningMessage);
|
report('warn', warningMessage);
|
||||||
} else {
|
} else {
|
||||||
// Server is running but model not found
|
// Server is running but model not found
|
||||||
const tagsUrl = `${ollamaBaseURL}/tags`;
|
const tagsUrl = `${ollamaBaseURL}/tags`;
|
||||||
@@ -489,11 +489,11 @@ async function setModel(role, modelId, options = {}) {
|
|||||||
`Model ID "${modelId}" not found in the Ollama instance. Please verify the model is pulled and available. You can check available models with: curl ${tagsUrl}`
|
`Model ID "${modelId}" not found in the Ollama instance. Please verify the model is pulled and available. You can check available models with: curl ${tagsUrl}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (providerHint === "bedrock") {
|
} else if (providerHint === 'bedrock') {
|
||||||
// Set provider without model validation since Bedrock models are managed by AWS
|
// Set provider without model validation since Bedrock models are managed by AWS
|
||||||
determinedProvider = "bedrock";
|
determinedProvider = 'bedrock';
|
||||||
warningMessage = `Warning: Custom Bedrock model '${modelId}' set. Please ensure the model ID is valid and accessible in your AWS account.`;
|
warningMessage = `Warning: Custom Bedrock model '${modelId}' set. Please ensure the model ID is valid and accessible in your AWS account.`;
|
||||||
report("warn", warningMessage);
|
report('warn', warningMessage);
|
||||||
} else {
|
} else {
|
||||||
// Invalid provider hint - should not happen
|
// Invalid provider hint - should not happen
|
||||||
throw new Error(`Invalid provider hint received: ${providerHint}`);
|
throw new Error(`Invalid provider hint received: ${providerHint}`);
|
||||||
@@ -505,7 +505,7 @@ async function setModel(role, modelId, options = {}) {
|
|||||||
// Found internally, use the provider from the internal list
|
// Found internally, use the provider from the internal list
|
||||||
determinedProvider = modelData.provider;
|
determinedProvider = modelData.provider;
|
||||||
report(
|
report(
|
||||||
"info",
|
'info',
|
||||||
`Model ${modelId} found internally with provider ${determinedProvider}.`
|
`Model ${modelId} found internally with provider ${determinedProvider}.`
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -513,9 +513,9 @@ async function setModel(role, modelId, options = {}) {
|
|||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: {
|
error: {
|
||||||
code: "MODEL_NOT_FOUND_NO_HINT",
|
code: 'MODEL_NOT_FOUND_NO_HINT',
|
||||||
message: `Model ID "${modelId}" not found in Taskmaster's supported models. If this is a custom model, please specify the provider using --openrouter or --ollama.`,
|
message: `Model ID "${modelId}" not found in Taskmaster's supported models. If this is a custom model, please specify the provider using --openrouter or --ollama.`
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -528,9 +528,9 @@ async function setModel(role, modelId, options = {}) {
|
|||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: {
|
error: {
|
||||||
code: "PROVIDER_UNDETERMINED",
|
code: 'PROVIDER_UNDETERMINED',
|
||||||
message: `Could not determine the provider for model ID "${modelId}".`,
|
message: `Could not determine the provider for model ID "${modelId}".`
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -538,7 +538,7 @@ async function setModel(role, modelId, options = {}) {
|
|||||||
currentConfig.models[role] = {
|
currentConfig.models[role] = {
|
||||||
...currentConfig.models[role], // Keep existing params like maxTokens
|
...currentConfig.models[role], // Keep existing params like maxTokens
|
||||||
provider: determinedProvider,
|
provider: determinedProvider,
|
||||||
modelId: modelId,
|
modelId: modelId
|
||||||
};
|
};
|
||||||
|
|
||||||
// Write updated configuration
|
// Write updated configuration
|
||||||
@@ -547,14 +547,14 @@ async function setModel(role, modelId, options = {}) {
|
|||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: {
|
error: {
|
||||||
code: "CONFIG_WRITE_ERROR",
|
code: 'CONFIG_WRITE_ERROR',
|
||||||
message: "Error writing updated configuration to configuration file",
|
message: 'Error writing updated configuration to configuration file'
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const successMessage = `Successfully set ${role} model to ${modelId} (Provider: ${determinedProvider})`;
|
const successMessage = `Successfully set ${role} model to ${modelId} (Provider: ${determinedProvider})`;
|
||||||
report("info", successMessage);
|
report('info', successMessage);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -563,17 +563,17 @@ async function setModel(role, modelId, options = {}) {
|
|||||||
provider: determinedProvider,
|
provider: determinedProvider,
|
||||||
modelId,
|
modelId,
|
||||||
message: successMessage,
|
message: successMessage,
|
||||||
warning: warningMessage, // Include warning in the response data
|
warning: warningMessage // Include warning in the response data
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
report("error", `Error setting ${role} model: ${error.message}`);
|
report('error', `Error setting ${role} model: ${error.message}`);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: {
|
error: {
|
||||||
code: "SET_MODEL_ERROR",
|
code: 'SET_MODEL_ERROR',
|
||||||
message: error.message,
|
message: error.message
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -589,7 +589,7 @@ async function setModel(role, modelId, options = {}) {
|
|||||||
async function getApiKeyStatusReport(options = {}) {
|
async function getApiKeyStatusReport(options = {}) {
|
||||||
const { mcpLog, projectRoot, session } = options;
|
const { mcpLog, projectRoot, session } = options;
|
||||||
const report = (level, ...args) => {
|
const report = (level, ...args) => {
|
||||||
if (mcpLog && typeof mcpLog[level] === "function") {
|
if (mcpLog && typeof mcpLog[level] === 'function') {
|
||||||
mcpLog[level](...args);
|
mcpLog[level](...args);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -597,7 +597,7 @@ async function getApiKeyStatusReport(options = {}) {
|
|||||||
try {
|
try {
|
||||||
const providers = getAllProviders();
|
const providers = getAllProviders();
|
||||||
const providersToCheck = providers.filter(
|
const providersToCheck = providers.filter(
|
||||||
(p) => p.toLowerCase() !== "ollama"
|
(p) => p.toLowerCase() !== 'ollama'
|
||||||
); // Ollama is not a provider, it's a service, doesn't need an api key usually
|
); // Ollama is not a provider, it's a service, doesn't need an api key usually
|
||||||
const statusReport = providersToCheck.map((provider) => {
|
const statusReport = providersToCheck.map((provider) => {
|
||||||
// Use provided projectRoot for MCP status check
|
// Use provided projectRoot for MCP status check
|
||||||
@@ -606,26 +606,26 @@ async function getApiKeyStatusReport(options = {}) {
|
|||||||
return {
|
return {
|
||||||
provider,
|
provider,
|
||||||
cli: cliOk,
|
cli: cliOk,
|
||||||
mcp: mcpOk,
|
mcp: mcpOk
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
report("info", "Successfully generated API key status report.");
|
report('info', 'Successfully generated API key status report.');
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
report: statusReport,
|
report: statusReport,
|
||||||
message: "API key status report generated.",
|
message: 'API key status report generated.'
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
report("error", `Error generating API key status report: ${error.message}`);
|
report('error', `Error generating API key status report: ${error.message}`);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: {
|
error: {
|
||||||
code: "API_KEY_STATUS_ERROR",
|
code: 'API_KEY_STATUS_ERROR',
|
||||||
message: error.message,
|
message: error.message
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -634,5 +634,5 @@ export {
|
|||||||
getModelConfiguration,
|
getModelConfiguration,
|
||||||
getAvailableModelsList,
|
getAvailableModelsList,
|
||||||
setModel,
|
setModel,
|
||||||
getApiKeyStatusReport,
|
getApiKeyStatusReport
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import path from "path";
|
import path from 'path';
|
||||||
import chalk from "chalk";
|
import chalk from 'chalk';
|
||||||
import boxen from "boxen";
|
import boxen from 'boxen';
|
||||||
|
|
||||||
import { log, readJSON, writeJSON, findTaskById } from "../utils.js";
|
import { log, readJSON, writeJSON, findTaskById } from '../utils.js';
|
||||||
import { displayBanner } from "../ui.js";
|
import { displayBanner } from '../ui.js';
|
||||||
import { validateTaskDependencies } from "../dependency-manager.js";
|
import { validateTaskDependencies } from '../dependency-manager.js';
|
||||||
import { getDebugFlag } from "../config-manager.js";
|
import { getDebugFlag } from '../config-manager.js';
|
||||||
import updateSingleTaskStatus from "./update-single-task-status.js";
|
import updateSingleTaskStatus from './update-single-task-status.js';
|
||||||
import generateTaskFiles from "./generate-task-files.js";
|
import generateTaskFiles from './generate-task-files.js';
|
||||||
import {
|
import {
|
||||||
isValidTaskStatus,
|
isValidTaskStatus,
|
||||||
TASK_STATUS_OPTIONS,
|
TASK_STATUS_OPTIONS
|
||||||
} from "../../../src/constants/task-status.js";
|
} from '../../../src/constants/task-status.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the status of a task
|
* Set the status of a task
|
||||||
@@ -25,7 +25,7 @@ async function setTaskStatus(tasksPath, taskIdInput, newStatus, options = {}) {
|
|||||||
try {
|
try {
|
||||||
if (!isValidTaskStatus(newStatus)) {
|
if (!isValidTaskStatus(newStatus)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Error: Invalid status value: ${newStatus}. Use one of: ${TASK_STATUS_OPTIONS.join(", ")}`
|
`Error: Invalid status value: ${newStatus}. Use one of: ${TASK_STATUS_OPTIONS.join(', ')}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Determine if we're in MCP mode by checking for mcpLog
|
// Determine if we're in MCP mode by checking for mcpLog
|
||||||
@@ -36,20 +36,20 @@ async function setTaskStatus(tasksPath, taskIdInput, newStatus, options = {}) {
|
|||||||
console.log(
|
console.log(
|
||||||
boxen(chalk.white.bold(`Updating Task Status to: ${newStatus}`), {
|
boxen(chalk.white.bold(`Updating Task Status to: ${newStatus}`), {
|
||||||
padding: 1,
|
padding: 1,
|
||||||
borderColor: "blue",
|
borderColor: 'blue',
|
||||||
borderStyle: "round",
|
borderStyle: 'round'
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
log("info", `Reading tasks from ${tasksPath}...`);
|
log('info', `Reading tasks from ${tasksPath}...`);
|
||||||
const data = readJSON(tasksPath);
|
const data = readJSON(tasksPath);
|
||||||
if (!data || !data.tasks) {
|
if (!data || !data.tasks) {
|
||||||
throw new Error(`No valid tasks found in ${tasksPath}`);
|
throw new Error(`No valid tasks found in ${tasksPath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle multiple task IDs (comma-separated)
|
// Handle multiple task IDs (comma-separated)
|
||||||
const taskIds = taskIdInput.split(",").map((id) => id.trim());
|
const taskIds = taskIdInput.split(',').map((id) => id.trim());
|
||||||
const updatedTasks = [];
|
const updatedTasks = [];
|
||||||
|
|
||||||
// Update each task
|
// Update each task
|
||||||
@@ -62,13 +62,13 @@ async function setTaskStatus(tasksPath, taskIdInput, newStatus, options = {}) {
|
|||||||
writeJSON(tasksPath, data);
|
writeJSON(tasksPath, data);
|
||||||
|
|
||||||
// Validate dependencies after status update
|
// Validate dependencies after status update
|
||||||
log("info", "Validating dependencies after status update...");
|
log('info', 'Validating dependencies after status update...');
|
||||||
validateTaskDependencies(data.tasks);
|
validateTaskDependencies(data.tasks);
|
||||||
|
|
||||||
// Generate individual task files
|
// Generate individual task files
|
||||||
log("info", "Regenerating task files...");
|
log('info', 'Regenerating task files...');
|
||||||
await generateTaskFiles(tasksPath, path.dirname(tasksPath), {
|
await generateTaskFiles(tasksPath, path.dirname(tasksPath), {
|
||||||
mcpLog: options.mcpLog,
|
mcpLog: options.mcpLog
|
||||||
});
|
});
|
||||||
|
|
||||||
// Display success message - only in CLI mode
|
// Display success message - only in CLI mode
|
||||||
@@ -80,10 +80,10 @@ async function setTaskStatus(tasksPath, taskIdInput, newStatus, options = {}) {
|
|||||||
console.log(
|
console.log(
|
||||||
boxen(
|
boxen(
|
||||||
chalk.white.bold(`Successfully updated task ${id} status:`) +
|
chalk.white.bold(`Successfully updated task ${id} status:`) +
|
||||||
"\n" +
|
'\n' +
|
||||||
`From: ${chalk.yellow(task ? task.status : "unknown")}\n` +
|
`From: ${chalk.yellow(task ? task.status : 'unknown')}\n` +
|
||||||
`To: ${chalk.green(newStatus)}`,
|
`To: ${chalk.green(newStatus)}`,
|
||||||
{ padding: 1, borderColor: "green", borderStyle: "round" }
|
{ padding: 1, borderColor: 'green', borderStyle: 'round' }
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -94,11 +94,11 @@ async function setTaskStatus(tasksPath, taskIdInput, newStatus, options = {}) {
|
|||||||
success: true,
|
success: true,
|
||||||
updatedTasks: updatedTasks.map((id) => ({
|
updatedTasks: updatedTasks.map((id) => ({
|
||||||
id,
|
id,
|
||||||
status: newStatus,
|
status: newStatus
|
||||||
})),
|
}))
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log("error", `Error setting task status: ${error.message}`);
|
log('error', `Error setting task status: ${error.message}`);
|
||||||
|
|
||||||
// Only show error UI in CLI mode
|
// Only show error UI in CLI mode
|
||||||
if (!options?.mcpLog) {
|
if (!options?.mcpLog) {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
|||||||
import { generateText, streamText, generateObject } from "ai";
|
import { generateText, streamText, generateObject } from 'ai';
|
||||||
import { log } from "../../scripts/modules/index.js";
|
import { log } from '../../scripts/modules/index.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base class for all AI providers
|
* Base class for all AI providers
|
||||||
@@ -7,7 +7,7 @@ import { log } from "../../scripts/modules/index.js";
|
|||||||
export class BaseAIProvider {
|
export class BaseAIProvider {
|
||||||
constructor() {
|
constructor() {
|
||||||
if (this.constructor === BaseAIProvider) {
|
if (this.constructor === BaseAIProvider) {
|
||||||
throw new Error("BaseAIProvider cannot be instantiated directly");
|
throw new Error('BaseAIProvider cannot be instantiated directly');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Each provider must set their name
|
// Each provider must set their name
|
||||||
@@ -51,10 +51,10 @@ export class BaseAIProvider {
|
|||||||
params.temperature !== undefined &&
|
params.temperature !== undefined &&
|
||||||
(params.temperature < 0 || params.temperature > 1)
|
(params.temperature < 0 || params.temperature > 1)
|
||||||
) {
|
) {
|
||||||
throw new Error("Temperature must be between 0 and 1");
|
throw new Error('Temperature must be between 0 and 1');
|
||||||
}
|
}
|
||||||
if (params.maxTokens !== undefined && params.maxTokens <= 0) {
|
if (params.maxTokens !== undefined && params.maxTokens <= 0) {
|
||||||
throw new Error("maxTokens must be greater than 0");
|
throw new Error('maxTokens must be greater than 0');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,13 +63,13 @@ export class BaseAIProvider {
|
|||||||
*/
|
*/
|
||||||
validateMessages(messages) {
|
validateMessages(messages) {
|
||||||
if (!messages || !Array.isArray(messages) || messages.length === 0) {
|
if (!messages || !Array.isArray(messages) || messages.length === 0) {
|
||||||
throw new Error("Invalid or empty messages array provided");
|
throw new Error('Invalid or empty messages array provided');
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const msg of messages) {
|
for (const msg of messages) {
|
||||||
if (!msg.role || !msg.content) {
|
if (!msg.role || !msg.content) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Invalid message format. Each message must have role and content"
|
'Invalid message format. Each message must have role and content'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,9 +79,9 @@ export class BaseAIProvider {
|
|||||||
* Common error handler
|
* Common error handler
|
||||||
*/
|
*/
|
||||||
handleError(operation, error) {
|
handleError(operation, error) {
|
||||||
const errorMessage = error.message || "Unknown error occurred";
|
const errorMessage = error.message || 'Unknown error occurred';
|
||||||
log("error", `${this.name} ${operation} failed: ${errorMessage}`, {
|
log('error', `${this.name} ${operation} failed: ${errorMessage}`, {
|
||||||
error,
|
error
|
||||||
});
|
});
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`${this.name} API error during ${operation}: ${errorMessage}`
|
`${this.name} API error during ${operation}: ${errorMessage}`
|
||||||
@@ -93,7 +93,7 @@ export class BaseAIProvider {
|
|||||||
* @abstract
|
* @abstract
|
||||||
*/
|
*/
|
||||||
getClient(params) {
|
getClient(params) {
|
||||||
throw new Error("getClient must be implemented by provider");
|
throw new Error('getClient must be implemented by provider');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -105,7 +105,7 @@ export class BaseAIProvider {
|
|||||||
this.validateMessages(params.messages);
|
this.validateMessages(params.messages);
|
||||||
|
|
||||||
log(
|
log(
|
||||||
"debug",
|
'debug',
|
||||||
`Generating ${this.name} text with model: ${params.modelId}`
|
`Generating ${this.name} text with model: ${params.modelId}`
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -114,11 +114,11 @@ export class BaseAIProvider {
|
|||||||
model: client(params.modelId),
|
model: client(params.modelId),
|
||||||
messages: params.messages,
|
messages: params.messages,
|
||||||
maxTokens: params.maxTokens,
|
maxTokens: params.maxTokens,
|
||||||
temperature: params.temperature,
|
temperature: params.temperature
|
||||||
});
|
});
|
||||||
|
|
||||||
log(
|
log(
|
||||||
"debug",
|
'debug',
|
||||||
`${this.name} generateText completed successfully for model: ${params.modelId}`
|
`${this.name} generateText completed successfully for model: ${params.modelId}`
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -127,11 +127,11 @@ export class BaseAIProvider {
|
|||||||
usage: {
|
usage: {
|
||||||
inputTokens: result.usage?.promptTokens,
|
inputTokens: result.usage?.promptTokens,
|
||||||
outputTokens: result.usage?.completionTokens,
|
outputTokens: result.usage?.completionTokens,
|
||||||
totalTokens: result.usage?.totalTokens,
|
totalTokens: result.usage?.totalTokens
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.handleError("text generation", error);
|
this.handleError('text generation', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,24 +143,24 @@ export class BaseAIProvider {
|
|||||||
this.validateParams(params);
|
this.validateParams(params);
|
||||||
this.validateMessages(params.messages);
|
this.validateMessages(params.messages);
|
||||||
|
|
||||||
log("debug", `Streaming ${this.name} text with model: ${params.modelId}`);
|
log('debug', `Streaming ${this.name} text with model: ${params.modelId}`);
|
||||||
|
|
||||||
const client = this.getClient(params);
|
const client = this.getClient(params);
|
||||||
const stream = await streamText({
|
const stream = await streamText({
|
||||||
model: client(params.modelId),
|
model: client(params.modelId),
|
||||||
messages: params.messages,
|
messages: params.messages,
|
||||||
maxTokens: params.maxTokens,
|
maxTokens: params.maxTokens,
|
||||||
temperature: params.temperature,
|
temperature: params.temperature
|
||||||
});
|
});
|
||||||
|
|
||||||
log(
|
log(
|
||||||
"debug",
|
'debug',
|
||||||
`${this.name} streamText initiated successfully for model: ${params.modelId}`
|
`${this.name} streamText initiated successfully for model: ${params.modelId}`
|
||||||
);
|
);
|
||||||
|
|
||||||
return stream;
|
return stream;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.handleError("text streaming", error);
|
this.handleError('text streaming', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,14 +173,14 @@ export class BaseAIProvider {
|
|||||||
this.validateMessages(params.messages);
|
this.validateMessages(params.messages);
|
||||||
|
|
||||||
if (!params.schema) {
|
if (!params.schema) {
|
||||||
throw new Error("Schema is required for object generation");
|
throw new Error('Schema is required for object generation');
|
||||||
}
|
}
|
||||||
if (!params.objectName) {
|
if (!params.objectName) {
|
||||||
throw new Error("Object name is required for object generation");
|
throw new Error('Object name is required for object generation');
|
||||||
}
|
}
|
||||||
|
|
||||||
log(
|
log(
|
||||||
"debug",
|
'debug',
|
||||||
`Generating ${this.name} object ('${params.objectName}') with model: ${params.modelId}`
|
`Generating ${this.name} object ('${params.objectName}') with model: ${params.modelId}`
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -189,13 +189,13 @@ export class BaseAIProvider {
|
|||||||
model: client(params.modelId),
|
model: client(params.modelId),
|
||||||
messages: params.messages,
|
messages: params.messages,
|
||||||
schema: params.schema,
|
schema: params.schema,
|
||||||
mode: "auto",
|
mode: 'auto',
|
||||||
maxTokens: params.maxTokens,
|
maxTokens: params.maxTokens,
|
||||||
temperature: params.temperature,
|
temperature: params.temperature
|
||||||
});
|
});
|
||||||
|
|
||||||
log(
|
log(
|
||||||
"debug",
|
'debug',
|
||||||
`${this.name} generateObject completed successfully for model: ${params.modelId}`
|
`${this.name} generateObject completed successfully for model: ${params.modelId}`
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -204,11 +204,11 @@ export class BaseAIProvider {
|
|||||||
usage: {
|
usage: {
|
||||||
inputTokens: result.usage?.promptTokens,
|
inputTokens: result.usage?.promptTokens,
|
||||||
outputTokens: result.usage?.completionTokens,
|
outputTokens: result.usage?.completionTokens,
|
||||||
totalTokens: result.usage?.totalTokens,
|
totalTokens: result.usage?.totalTokens
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.handleError("object generation", error);
|
this.handleError('object generation', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,145 +1,149 @@
|
|||||||
/**
|
/**
|
||||||
* Tests for the add-task.js module
|
* Tests for the add-task.js module
|
||||||
*/
|
*/
|
||||||
import { jest } from '@jest/globals';
|
import { jest } from "@jest/globals";
|
||||||
|
|
||||||
// Mock the dependencies before importing the module under test
|
// Mock the dependencies before importing the module under test
|
||||||
jest.unstable_mockModule('../../../../../scripts/modules/utils.js', () => ({
|
jest.unstable_mockModule("../../../../../scripts/modules/utils.js", () => ({
|
||||||
readJSON: jest.fn(),
|
readJSON: jest.fn(),
|
||||||
writeJSON: jest.fn(),
|
writeJSON: jest.fn(),
|
||||||
log: jest.fn(),
|
log: jest.fn(),
|
||||||
CONFIG: {
|
CONFIG: {
|
||||||
model: 'mock-claude-model',
|
model: "mock-claude-model",
|
||||||
maxTokens: 4000,
|
maxTokens: 4000,
|
||||||
temperature: 0.7,
|
temperature: 0.7,
|
||||||
debug: false
|
debug: false,
|
||||||
},
|
},
|
||||||
truncate: jest.fn((text) => text)
|
truncate: jest.fn((text) => text),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.unstable_mockModule('../../../../../scripts/modules/ui.js', () => ({
|
jest.unstable_mockModule("../../../../../scripts/modules/ui.js", () => ({
|
||||||
displayBanner: jest.fn(),
|
displayBanner: jest.fn(),
|
||||||
getStatusWithColor: jest.fn((status) => status),
|
getStatusWithColor: jest.fn((status) => status),
|
||||||
startLoadingIndicator: jest.fn(),
|
startLoadingIndicator: jest.fn(),
|
||||||
stopLoadingIndicator: jest.fn(),
|
stopLoadingIndicator: jest.fn(),
|
||||||
displayAiUsageSummary: jest.fn()
|
succeedLoadingIndicator: jest.fn(),
|
||||||
|
failLoadingIndicator: jest.fn(),
|
||||||
|
warnLoadingIndicator: jest.fn(),
|
||||||
|
infoLoadingIndicator: jest.fn(),
|
||||||
|
displayAiUsageSummary: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.unstable_mockModule(
|
jest.unstable_mockModule(
|
||||||
'../../../../../scripts/modules/ai-services-unified.js',
|
"../../../../../scripts/modules/ai-services-unified.js",
|
||||||
() => ({
|
() => ({
|
||||||
generateObjectService: jest.fn().mockResolvedValue({
|
generateObjectService: jest.fn().mockResolvedValue({
|
||||||
mainResult: {
|
mainResult: {
|
||||||
object: {
|
object: {
|
||||||
title: 'Task from prompt: Create a new authentication system',
|
title: "Task from prompt: Create a new authentication system",
|
||||||
description:
|
description:
|
||||||
'Task generated from: Create a new authentication system',
|
"Task generated from: Create a new authentication system",
|
||||||
details:
|
details:
|
||||||
'Implementation details for task generated from prompt: Create a new authentication system',
|
"Implementation details for task generated from prompt: Create a new authentication system",
|
||||||
testStrategy: 'Write unit tests to verify functionality',
|
testStrategy: "Write unit tests to verify functionality",
|
||||||
dependencies: []
|
dependencies: [],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
telemetryData: {
|
telemetryData: {
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
userId: '1234567890',
|
userId: "1234567890",
|
||||||
commandName: 'add-task',
|
commandName: "add-task",
|
||||||
modelUsed: 'claude-3-5-sonnet',
|
modelUsed: "claude-3-5-sonnet",
|
||||||
providerName: 'anthropic',
|
providerName: "anthropic",
|
||||||
inputTokens: 1000,
|
inputTokens: 1000,
|
||||||
outputTokens: 500,
|
outputTokens: 500,
|
||||||
totalTokens: 1500,
|
totalTokens: 1500,
|
||||||
totalCost: 0.012414,
|
totalCost: 0.012414,
|
||||||
currency: 'USD'
|
currency: "USD",
|
||||||
}
|
},
|
||||||
})
|
}),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
jest.unstable_mockModule(
|
jest.unstable_mockModule(
|
||||||
'../../../../../scripts/modules/config-manager.js',
|
"../../../../../scripts/modules/config-manager.js",
|
||||||
() => ({
|
() => ({
|
||||||
getDefaultPriority: jest.fn(() => 'medium')
|
getDefaultPriority: jest.fn(() => "medium"),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
jest.unstable_mockModule(
|
jest.unstable_mockModule(
|
||||||
'../../../../../scripts/modules/task-manager/generate-task-files.js',
|
"../../../../../scripts/modules/task-manager/generate-task-files.js",
|
||||||
() => ({
|
() => ({
|
||||||
default: jest.fn().mockResolvedValue()
|
default: jest.fn().mockResolvedValue(),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Mock external UI libraries
|
// Mock external UI libraries
|
||||||
jest.unstable_mockModule('chalk', () => ({
|
jest.unstable_mockModule("chalk", () => ({
|
||||||
default: {
|
default: {
|
||||||
white: { bold: jest.fn((text) => text) },
|
white: { bold: jest.fn((text) => text) },
|
||||||
cyan: Object.assign(
|
cyan: Object.assign(
|
||||||
jest.fn((text) => text),
|
jest.fn((text) => text),
|
||||||
{
|
{
|
||||||
bold: jest.fn((text) => text)
|
bold: jest.fn((text) => text),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
green: jest.fn((text) => text),
|
green: jest.fn((text) => text),
|
||||||
yellow: jest.fn((text) => text),
|
yellow: jest.fn((text) => text),
|
||||||
bold: jest.fn((text) => text)
|
bold: jest.fn((text) => text),
|
||||||
}
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.unstable_mockModule('boxen', () => ({
|
jest.unstable_mockModule("boxen", () => ({
|
||||||
default: jest.fn((text) => text)
|
default: jest.fn((text) => text),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.unstable_mockModule('cli-table3', () => ({
|
jest.unstable_mockModule("cli-table3", () => ({
|
||||||
default: jest.fn().mockImplementation(() => ({
|
default: jest.fn().mockImplementation(() => ({
|
||||||
push: jest.fn(),
|
push: jest.fn(),
|
||||||
toString: jest.fn(() => 'mocked table')
|
toString: jest.fn(() => "mocked table"),
|
||||||
}))
|
})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Import the mocked modules
|
// Import the mocked modules
|
||||||
const { readJSON, writeJSON, log } = await import(
|
const { readJSON, writeJSON, log } = await import(
|
||||||
'../../../../../scripts/modules/utils.js'
|
"../../../../../scripts/modules/utils.js"
|
||||||
);
|
);
|
||||||
|
|
||||||
const { generateObjectService } = await import(
|
const { generateObjectService } = await import(
|
||||||
'../../../../../scripts/modules/ai-services-unified.js'
|
"../../../../../scripts/modules/ai-services-unified.js"
|
||||||
);
|
);
|
||||||
|
|
||||||
const generateTaskFiles = await import(
|
const generateTaskFiles = await import(
|
||||||
'../../../../../scripts/modules/task-manager/generate-task-files.js'
|
"../../../../../scripts/modules/task-manager/generate-task-files.js"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Import the module under test
|
// Import the module under test
|
||||||
const { default: addTask } = await import(
|
const { default: addTask } = await import(
|
||||||
'../../../../../scripts/modules/task-manager/add-task.js'
|
"../../../../../scripts/modules/task-manager/add-task.js"
|
||||||
);
|
);
|
||||||
|
|
||||||
describe('addTask', () => {
|
describe("addTask", () => {
|
||||||
const sampleTasks = {
|
const sampleTasks = {
|
||||||
tasks: [
|
tasks: [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
title: 'Task 1',
|
title: "Task 1",
|
||||||
description: 'First task',
|
description: "First task",
|
||||||
status: 'pending',
|
status: "pending",
|
||||||
dependencies: []
|
dependencies: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
title: 'Task 2',
|
title: "Task 2",
|
||||||
description: 'Second task',
|
description: "Second task",
|
||||||
status: 'pending',
|
status: "pending",
|
||||||
dependencies: []
|
dependencies: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
title: 'Task 3',
|
title: "Task 3",
|
||||||
description: 'Third task',
|
description: "Third task",
|
||||||
status: 'pending',
|
status: "pending",
|
||||||
dependencies: [1]
|
dependencies: [1],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create a helper function for consistent mcpLog mock
|
// Create a helper function for consistent mcpLog mock
|
||||||
@@ -148,7 +152,7 @@ describe('addTask', () => {
|
|||||||
warn: jest.fn(),
|
warn: jest.fn(),
|
||||||
error: jest.fn(),
|
error: jest.fn(),
|
||||||
debug: jest.fn(),
|
debug: jest.fn(),
|
||||||
success: jest.fn()
|
success: jest.fn(),
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -156,195 +160,195 @@ describe('addTask', () => {
|
|||||||
readJSON.mockReturnValue(JSON.parse(JSON.stringify(sampleTasks)));
|
readJSON.mockReturnValue(JSON.parse(JSON.stringify(sampleTasks)));
|
||||||
|
|
||||||
// Mock console.log to avoid output during tests
|
// Mock console.log to avoid output during tests
|
||||||
jest.spyOn(console, 'log').mockImplementation(() => {});
|
jest.spyOn(console, "log").mockImplementation(() => {});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
console.log.mockRestore();
|
console.log.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should add a new task using AI', async () => {
|
test("should add a new task using AI", async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const prompt = 'Create a new authentication system';
|
const prompt = "Create a new authentication system";
|
||||||
const context = {
|
const context = {
|
||||||
mcpLog: createMcpLogMock()
|
mcpLog: createMcpLogMock(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const result = await addTask(
|
const result = await addTask(
|
||||||
'tasks/tasks.json',
|
"tasks/tasks.json",
|
||||||
prompt,
|
prompt,
|
||||||
[],
|
[],
|
||||||
'medium',
|
"medium",
|
||||||
context,
|
context,
|
||||||
'json'
|
"json"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(readJSON).toHaveBeenCalledWith('tasks/tasks.json');
|
expect(readJSON).toHaveBeenCalledWith("tasks/tasks.json");
|
||||||
expect(generateObjectService).toHaveBeenCalledWith(expect.any(Object));
|
expect(generateObjectService).toHaveBeenCalledWith(expect.any(Object));
|
||||||
expect(writeJSON).toHaveBeenCalledWith(
|
expect(writeJSON).toHaveBeenCalledWith(
|
||||||
'tasks/tasks.json',
|
"tasks/tasks.json",
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
tasks: expect.arrayContaining([
|
tasks: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: 4, // Next ID after existing tasks
|
id: 4, // Next ID after existing tasks
|
||||||
title: expect.stringContaining(
|
title: expect.stringContaining(
|
||||||
'Create a new authentication system'
|
"Create a new authentication system"
|
||||||
),
|
),
|
||||||
status: 'pending'
|
status: "pending",
|
||||||
})
|
}),
|
||||||
])
|
]),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
expect(generateTaskFiles.default).toHaveBeenCalled();
|
expect(generateTaskFiles.default).toHaveBeenCalled();
|
||||||
expect(result).toEqual(
|
expect(result).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
newTaskId: 4,
|
newTaskId: 4,
|
||||||
telemetryData: expect.any(Object)
|
telemetryData: expect.any(Object),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should validate dependencies when adding a task', async () => {
|
test("should validate dependencies when adding a task", async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const prompt = 'Create a new authentication system';
|
const prompt = "Create a new authentication system";
|
||||||
const validDependencies = [1, 2]; // These exist in sampleTasks
|
const validDependencies = [1, 2]; // These exist in sampleTasks
|
||||||
const context = {
|
const context = {
|
||||||
mcpLog: createMcpLogMock()
|
mcpLog: createMcpLogMock(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const result = await addTask(
|
const result = await addTask(
|
||||||
'tasks/tasks.json',
|
"tasks/tasks.json",
|
||||||
prompt,
|
prompt,
|
||||||
validDependencies,
|
validDependencies,
|
||||||
'medium',
|
"medium",
|
||||||
context,
|
context,
|
||||||
'json'
|
"json"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(writeJSON).toHaveBeenCalledWith(
|
expect(writeJSON).toHaveBeenCalledWith(
|
||||||
'tasks/tasks.json',
|
"tasks/tasks.json",
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
tasks: expect.arrayContaining([
|
tasks: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: 4,
|
id: 4,
|
||||||
dependencies: validDependencies
|
dependencies: validDependencies,
|
||||||
})
|
}),
|
||||||
])
|
]),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should filter out invalid dependencies', async () => {
|
test("should filter out invalid dependencies", async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const prompt = 'Create a new authentication system';
|
const prompt = "Create a new authentication system";
|
||||||
const invalidDependencies = [999]; // Non-existent task ID
|
const invalidDependencies = [999]; // Non-existent task ID
|
||||||
const context = { mcpLog: createMcpLogMock() };
|
const context = { mcpLog: createMcpLogMock() };
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const result = await addTask(
|
const result = await addTask(
|
||||||
'tasks/tasks.json',
|
"tasks/tasks.json",
|
||||||
prompt,
|
prompt,
|
||||||
invalidDependencies,
|
invalidDependencies,
|
||||||
'medium',
|
"medium",
|
||||||
context,
|
context,
|
||||||
'json'
|
"json"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(writeJSON).toHaveBeenCalledWith(
|
expect(writeJSON).toHaveBeenCalledWith(
|
||||||
'tasks/tasks.json',
|
"tasks/tasks.json",
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
tasks: expect.arrayContaining([
|
tasks: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: 4,
|
id: 4,
|
||||||
dependencies: [] // Invalid dependencies should be filtered out
|
dependencies: [], // Invalid dependencies should be filtered out
|
||||||
})
|
}),
|
||||||
])
|
]),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
expect(context.mcpLog.warn).toHaveBeenCalledWith(
|
expect(context.mcpLog.warn).toHaveBeenCalledWith(
|
||||||
expect.stringContaining(
|
expect.stringContaining(
|
||||||
'The following dependencies do not exist or are invalid: 999'
|
"The following dependencies do not exist or are invalid: 999"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should use specified priority', async () => {
|
test("should use specified priority", async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const prompt = 'Create a new authentication system';
|
const prompt = "Create a new authentication system";
|
||||||
const priority = 'high';
|
const priority = "high";
|
||||||
const context = {
|
const context = {
|
||||||
mcpLog: createMcpLogMock()
|
mcpLog: createMcpLogMock(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await addTask('tasks/tasks.json', prompt, [], priority, context, 'json');
|
await addTask("tasks/tasks.json", prompt, [], priority, context, "json");
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(writeJSON).toHaveBeenCalledWith(
|
expect(writeJSON).toHaveBeenCalledWith(
|
||||||
'tasks/tasks.json',
|
"tasks/tasks.json",
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
tasks: expect.arrayContaining([
|
tasks: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
priority: priority
|
priority: priority,
|
||||||
})
|
}),
|
||||||
])
|
]),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should handle empty tasks file', async () => {
|
test("should handle empty tasks file", async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
readJSON.mockReturnValue({ tasks: [] });
|
readJSON.mockReturnValue({ tasks: [] });
|
||||||
const prompt = 'Create a new authentication system';
|
const prompt = "Create a new authentication system";
|
||||||
const context = {
|
const context = {
|
||||||
mcpLog: createMcpLogMock()
|
mcpLog: createMcpLogMock(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const result = await addTask(
|
const result = await addTask(
|
||||||
'tasks/tasks.json',
|
"tasks/tasks.json",
|
||||||
prompt,
|
prompt,
|
||||||
[],
|
[],
|
||||||
'medium',
|
"medium",
|
||||||
context,
|
context,
|
||||||
'json'
|
"json"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(result.newTaskId).toBe(1); // First task should have ID 1
|
expect(result.newTaskId).toBe(1); // First task should have ID 1
|
||||||
expect(writeJSON).toHaveBeenCalledWith(
|
expect(writeJSON).toHaveBeenCalledWith(
|
||||||
'tasks/tasks.json',
|
"tasks/tasks.json",
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
tasks: expect.arrayContaining([
|
tasks: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: 1
|
id: 1,
|
||||||
})
|
}),
|
||||||
])
|
]),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should handle missing tasks file', async () => {
|
test("should handle missing tasks file", async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
readJSON.mockReturnValue(null);
|
readJSON.mockReturnValue(null);
|
||||||
const prompt = 'Create a new authentication system';
|
const prompt = "Create a new authentication system";
|
||||||
const context = {
|
const context = {
|
||||||
mcpLog: createMcpLogMock()
|
mcpLog: createMcpLogMock(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const result = await addTask(
|
const result = await addTask(
|
||||||
'tasks/tasks.json',
|
"tasks/tasks.json",
|
||||||
prompt,
|
prompt,
|
||||||
[],
|
[],
|
||||||
'medium',
|
"medium",
|
||||||
context,
|
context,
|
||||||
'json'
|
"json"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
@@ -352,49 +356,49 @@ describe('addTask', () => {
|
|||||||
expect(writeJSON).toHaveBeenCalledTimes(2); // Once to create file, once to add task
|
expect(writeJSON).toHaveBeenCalledTimes(2); // Once to create file, once to add task
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should handle AI service errors', async () => {
|
test("should handle AI service errors", async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
generateObjectService.mockRejectedValueOnce(new Error('AI service failed'));
|
generateObjectService.mockRejectedValueOnce(new Error("AI service failed"));
|
||||||
const prompt = 'Create a new authentication system';
|
const prompt = "Create a new authentication system";
|
||||||
const context = {
|
const context = {
|
||||||
mcpLog: createMcpLogMock()
|
mcpLog: createMcpLogMock(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
await expect(
|
await expect(
|
||||||
addTask('tasks/tasks.json', prompt, [], 'medium', context, 'json')
|
addTask("tasks/tasks.json", prompt, [], "medium", context, "json")
|
||||||
).rejects.toThrow('AI service failed');
|
).rejects.toThrow("AI service failed");
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should handle file read errors', async () => {
|
test("should handle file read errors", async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
readJSON.mockImplementation(() => {
|
readJSON.mockImplementation(() => {
|
||||||
throw new Error('File read failed');
|
throw new Error("File read failed");
|
||||||
});
|
});
|
||||||
const prompt = 'Create a new authentication system';
|
const prompt = "Create a new authentication system";
|
||||||
const context = {
|
const context = {
|
||||||
mcpLog: createMcpLogMock()
|
mcpLog: createMcpLogMock(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
await expect(
|
await expect(
|
||||||
addTask('tasks/tasks.json', prompt, [], 'medium', context, 'json')
|
addTask("tasks/tasks.json", prompt, [], "medium", context, "json")
|
||||||
).rejects.toThrow('File read failed');
|
).rejects.toThrow("File read failed");
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should handle file write errors', async () => {
|
test("should handle file write errors", async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
writeJSON.mockImplementation(() => {
|
writeJSON.mockImplementation(() => {
|
||||||
throw new Error('File write failed');
|
throw new Error("File write failed");
|
||||||
});
|
});
|
||||||
const prompt = 'Create a new authentication system';
|
const prompt = "Create a new authentication system";
|
||||||
const context = {
|
const context = {
|
||||||
mcpLog: createMcpLogMock()
|
mcpLog: createMcpLogMock(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
await expect(
|
await expect(
|
||||||
addTask('tasks/tasks.json', prompt, [], 'medium', context, 'json')
|
addTask("tasks/tasks.json", prompt, [], "medium", context, "json")
|
||||||
).rejects.toThrow('File write failed');
|
).rejects.toThrow("File write failed");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -82,19 +82,19 @@ describe("UI Module", () => {
|
|||||||
test("should return done status with emoji for console output", () => {
|
test("should return done status with emoji for console output", () => {
|
||||||
const result = getStatusWithColor("done");
|
const result = getStatusWithColor("done");
|
||||||
expect(result).toMatch(/done/);
|
expect(result).toMatch(/done/);
|
||||||
expect(result).toContain("✅");
|
expect(result).toContain("✓");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should return pending status with emoji for console output", () => {
|
test("should return pending status with emoji for console output", () => {
|
||||||
const result = getStatusWithColor("pending");
|
const result = getStatusWithColor("pending");
|
||||||
expect(result).toMatch(/pending/);
|
expect(result).toMatch(/pending/);
|
||||||
expect(result).toContain("⏱️");
|
expect(result).toContain("○");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should return deferred status with emoji for console output", () => {
|
test("should return deferred status with emoji for console output", () => {
|
||||||
const result = getStatusWithColor("deferred");
|
const result = getStatusWithColor("deferred");
|
||||||
expect(result).toMatch(/deferred/);
|
expect(result).toMatch(/deferred/);
|
||||||
expect(result).toContain("⏱️");
|
expect(result).toContain("x");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should return in-progress status with emoji for console output", () => {
|
test("should return in-progress status with emoji for console output", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user