Add list command with subtasks option and update documentation

This commit is contained in:
Eyal Toledano
2025-03-04 20:35:30 -05:00
parent 7a33979a62
commit aed8f5b3a0
6 changed files with 629 additions and 282 deletions

View File

@@ -1,8 +1,10 @@
# API Keys (Required)
ANTHROPIC_API_KEY=your_anthropic_api_key_here # Format: sk-ant-api03-...
PERPLEXITY_API_KEY=your_perplexity_api_key_here # Format: pplx-...
# Model Configuration
MODEL=claude-3-7-sonnet-20250219 # Recommended models: claude-3-7-sonnet-20250219, claude-3-opus-20240229
PERPLEXITY_MODEL=sonar-small-online # Perplexity model for research-backed subtasks
MAX_TOKENS=4000 # Maximum tokens for model responses
TEMPERATURE=0.7 # Temperature for model responses (0.0-1.0)

View File

@@ -7,6 +7,27 @@ A task management system for AI-driven development with Claude, designed to work
- Node.js 14.0.0 or higher
- Anthropic API key (Claude API)
- Anthropic SDK version 0.39.0 or higher
- OpenAI SDK (for Perplexity API integration, optional)
## Configuration
The script can be configured through environment variables in a `.env` file at the root of the project:
### Required Configuration
- `ANTHROPIC_API_KEY`: Your Anthropic API key for Claude
### Optional Configuration
- `MODEL`: Specify which Claude model to use (default: "claude-3-7-sonnet-20250219")
- `MAX_TOKENS`: Maximum tokens for model responses (default: 4000)
- `TEMPERATURE`: Temperature for model responses (default: 0.7)
- `PERPLEXITY_API_KEY`: Your Perplexity API key for research-backed subtask generation
- `PERPLEXITY_MODEL`: Specify which Perplexity model to use (default: "sonar-medium-online")
- `DEBUG`: Enable debug logging (default: false)
- `LOG_LEVEL`: Log level - debug, info, warn, error (default: info)
- `DEFAULT_SUBTASKS`: Default number of subtasks when expanding (default: 3)
- `DEFAULT_PRIORITY`: Default priority for generated tasks (default: medium)
- `PROJECT_NAME`: Override default project name in tasks.json
- `PROJECT_VERSION`: Override default version in tasks.json
## Installation
@@ -105,6 +126,7 @@ What tasks are available to work on next?
The agent will:
- Run `node scripts/dev.js list` to see all tasks
- Run `node scripts/dev.js list --with-subtasks` to see tasks with their subtasks
- Analyze dependencies to determine which tasks are ready to be worked on
- Prioritize tasks based on priority level and ID order
- Suggest the next task(s) to implement
@@ -194,6 +216,26 @@ The agent will execute:
node scripts/dev.js expand --all
```
For research-backed subtask generation using Perplexity AI:
```
Please break down task 5 using research-backed generation.
```
The agent will execute:
```bash
node scripts/dev.js expand --id=5 --research
```
You can also apply research-backed generation to all tasks:
```
Please break down all pending tasks using research-backed generation.
```
The agent will execute:
```bash
node scripts/dev.js expand --all --research
```
## Manual Command Reference
While the Cursor agent will handle most commands for you, you can also run them manually:
@@ -208,6 +250,21 @@ npm run parse-prd -- --input=<prd-file.txt>
npm run list
```
# List tasks with a specific status
```bash
npm run dev -- list --status=<status>
```
# List tasks with subtasks
```bash
npm run dev -- list --with-subtasks
```
# List tasks with a specific status and include subtasks
```bash
npm run dev -- list --status=<status> --with-subtasks
```
### Update Tasks
```bash
npm run dev -- update --from=<id> --prompt="<prompt>"
@@ -232,6 +289,15 @@ or
npm run dev -- expand --all
```
For research-backed subtask generation:
```bash
npm run dev -- expand --id=<id> --research
```
or
```bash
npm run dev -- expand --all --research
```
## Task Structure
Tasks in tasks.json have the following structure:

184
package-lock.json generated
View File

@@ -1,32 +1,40 @@
{
"name": "mcp-saas",
"name": "claude-task-master",
"version": "1.3.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mcp-saas",
"name": "claude-task-master",
"version": "1.3.1",
"license": "MIT",
"dependencies": {
"@anthropic-ai/sdk": "^0.16.0",
"dotenv": "^16.4.7"
"@anthropic-ai/sdk": "^0.39.0",
"chalk": "^4.1.2",
"commander": "^11.1.0",
"dotenv": "^16.3.1",
"openai": "^4.86.1"
},
"bin": {
"claude-task-init": "scripts/init.js"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@anthropic-ai/sdk": {
"version": "0.16.1",
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.16.1.tgz",
"integrity": "sha512-vHgvfWEyFy5ktqam56Nrhv8MVa7EJthsRYNi+1OrFFfyrj9tR2/aji1QbVbQjYU/pPhPFaYrdCEC/MLPFrmKwA==",
"version": "0.39.0",
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.39.0.tgz",
"integrity": "sha512-eMyDIPRZbt1CCLErRCi3exlAvNkBtRe+kW5vvJyef93PmNr/clstYgHhtvmkxN82nlKgzyGPCyGxrm0JQ1ZIdg==",
"license": "MIT",
"dependencies": {
"@types/node": "^18.11.18",
"@types/node-fetch": "^2.6.4",
"abort-controller": "^3.0.0",
"agentkeepalive": "^4.2.1",
"digest-fetch": "^1.3.0",
"form-data-encoder": "1.7.2",
"formdata-node": "^4.3.2",
"node-fetch": "^2.6.7",
"web-streams-polyfill": "^3.2.1"
"node-fetch": "^2.6.7"
}
},
"node_modules/@types/node": {
@@ -72,17 +80,27 @@
"node": ">= 8.0.0"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/base-64": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz",
"integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA=="
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
@@ -96,15 +114,40 @@
"node": ">= 0.4"
}
},
"node_modules/charenc": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
"integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==",
"license": "BSD-3-Clause",
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": "*"
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -117,13 +160,13 @@
"node": ">= 0.8"
}
},
"node_modules/crypt": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
"integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==",
"license": "BSD-3-Clause",
"node_modules/commander": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
"integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==",
"license": "MIT",
"engines": {
"node": "*"
"node": ">=16"
}
},
"node_modules/delayed-stream": {
@@ -135,16 +178,6 @@
"node": ">=0.4.0"
}
},
"node_modules/digest-fetch": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/digest-fetch/-/digest-fetch-1.3.0.tgz",
"integrity": "sha512-CGJuv6iKNM7QyZlM2T3sPAdZWd/p9zQiRNS9G+9COUCwzWFTs0Xp8NF5iePx7wtvhDykReiRRrSeNb4oMmB8lA==",
"license": "ISC",
"dependencies": {
"base-64": "^0.1.0",
"md5": "^2.3.0"
}
},
"node_modules/dotenv": {
"version": "16.4.7",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
@@ -326,6 +359,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
@@ -374,12 +416,6 @@
"ms": "^2.0.0"
}
},
"node_modules/is-buffer": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
"integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
"license": "MIT"
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -389,17 +425,6 @@
"node": ">= 0.4"
}
},
"node_modules/md5": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz",
"integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==",
"license": "BSD-3-Clause",
"dependencies": {
"charenc": "0.0.2",
"crypt": "0.0.2",
"is-buffer": "~1.1.6"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@@ -466,6 +491,48 @@
}
}
},
"node_modules/openai": {
"version": "4.86.1",
"resolved": "https://registry.npmjs.org/openai/-/openai-4.86.1.tgz",
"integrity": "sha512-x3iCLyaC3yegFVZaxOmrYJjitKxZ9hpVbLi+ZlT5UHuHTMlEQEbKXkGOM78z9qm2T5GF+XRUZCP2/aV4UPFPJQ==",
"license": "Apache-2.0",
"dependencies": {
"@types/node": "^18.11.18",
"@types/node-fetch": "^2.6.4",
"abort-controller": "^3.0.0",
"agentkeepalive": "^4.2.1",
"form-data-encoder": "1.7.2",
"formdata-node": "^4.3.2",
"node-fetch": "^2.6.7"
},
"bin": {
"openai": "bin/cli"
},
"peerDependencies": {
"ws": "^8.18.0",
"zod": "^3.23.8"
},
"peerDependenciesMeta": {
"ws": {
"optional": true
},
"zod": {
"optional": true
}
}
},
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
@@ -478,15 +545,6 @@
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"license": "MIT"
},
"node_modules/web-streams-polyfill": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
"license": "MIT",
"engines": {
"node": ">= 8"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "claude-task-master",
"version": "1.3.1",
"version": "1.3.2",
"description": "A task management system for AI-driven development with Claude",
"main": "index.js",
"type": "module",
@@ -28,7 +28,8 @@
"@anthropic-ai/sdk": "^0.39.0",
"chalk": "^4.1.2",
"commander": "^11.1.0",
"dotenv": "^16.3.1"
"dotenv": "^16.3.1",
"openai": "^4.86.1"
},
"engines": {
"node": ">=14.0.0"

View File

@@ -12,6 +12,7 @@ In an AI-driven development process—particularly with tools like [Cursor](http
4. **Generate** individual task files (e.g., `task_001.txt`) for easy reference or to feed into an AI coding workflow.
5. **Set task status**—mark tasks as `done`, `pending`, or `deferred` based on progress.
6. **Expand** tasks with subtasks—break down complex tasks into smaller, more manageable subtasks.
7. **Research-backed subtask generation**—use Perplexity AI to generate more informed and contextually relevant subtasks.
## Configuration
@@ -24,6 +25,8 @@ The script can be configured through environment variables in a `.env` file at t
- `MODEL`: Specify which Claude model to use (default: "claude-3-7-sonnet-20250219")
- `MAX_TOKENS`: Maximum tokens for model responses (default: 4000)
- `TEMPERATURE`: Temperature for model responses (default: 0.7)
- `PERPLEXITY_API_KEY`: Your Perplexity API key for research-backed subtask generation
- `PERPLEXITY_MODEL`: Specify which Perplexity model to use (default: "sonar-medium-online")
- `DEBUG`: Enable debug logging (default: false)
- `LOG_LEVEL`: Log level - debug, info, warn, error (default: info)
- `DEFAULT_SUBTASKS`: Default number of subtasks when expanding (default: 3)
@@ -56,6 +59,24 @@ The script can be configured through environment variables in a `.env` file at t
Run `node scripts/dev.js` without arguments to see detailed usage information.
## Listing Tasks
The `list` command allows you to view all tasks and their status:
```bash
# List all tasks
node scripts/dev.js list
# List tasks with a specific status
node scripts/dev.js list --status=pending
# List tasks and include their subtasks
node scripts/dev.js list --with-subtasks
# List tasks with a specific status and include their subtasks
node scripts/dev.js list --status=pending --with-subtasks
```
## Expanding Tasks
The `expand` command allows you to break down tasks into subtasks for more detailed implementation:
@@ -75,12 +96,35 @@ node scripts/dev.js expand --all
# Force regeneration of subtasks for all pending tasks
node scripts/dev.js expand --all --force
# Use Perplexity AI for research-backed subtask generation
node scripts/dev.js expand --id=3 --research
# Use Perplexity AI for research-backed generation on all pending tasks
node scripts/dev.js expand --all --research
```
Notes:
- Tasks marked as 'done' or 'completed' are always skipped
- By default, tasks that already have subtasks are skipped unless `--force` is used
- Subtasks include title, description, dependencies, and acceptance criteria
- The `--research` flag uses Perplexity AI to generate more informed and contextually relevant subtasks
- If Perplexity API is unavailable, the script will fall back to using Anthropic's Claude
## AI Integration
The script integrates with two AI services:
1. **Anthropic Claude**: Used for parsing PRDs, generating tasks, and creating subtasks.
2. **Perplexity AI**: Used for research-backed subtask generation when the `--research` flag is specified.
The Perplexity integration uses the OpenAI client to connect to Perplexity's API, which provides enhanced research capabilities for generating more informed subtasks. If the Perplexity API is unavailable or encounters an error, the script will automatically fall back to using Anthropic's Claude.
To use the Perplexity integration:
1. Obtain a Perplexity API key
2. Add `PERPLEXITY_API_KEY` to your `.env` file
3. Optionally specify `PERPLEXITY_MODEL` in your `.env` file (default: "sonar-medium-online")
4. Use the `--research` flag with the `expand` command
## Logging

View File

@@ -42,24 +42,43 @@
import fs from 'fs';
import path from 'path';
import dotenv from 'dotenv';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import readline from 'readline';
import { program } from 'commander';
import chalk from 'chalk';
import { Anthropic } from '@anthropic-ai/sdk';
import OpenAI from 'openai';
import dotenv from 'dotenv';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Load environment variables from .env file
// Load environment variables
dotenv.config();
import Anthropic from '@anthropic-ai/sdk';
// Configure Anthropic client
const anthropic = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
});
// Configure OpenAI client for Perplexity
const perplexity = new OpenAI({
apiKey: process.env.PERPLEXITY_API_KEY,
baseURL: 'https://api.perplexity.ai',
});
// Model configuration
const MODEL = process.env.MODEL || 'claude-3-7-sonnet-20250219';
const PERPLEXITY_MODEL = process.env.PERPLEXITY_MODEL || 'sonar-small-online';
const MAX_TOKENS = parseInt(process.env.MAX_TOKENS || '4000');
const TEMPERATURE = parseFloat(process.env.TEMPERATURE || '0.7');
// Set up configuration with environment variables or defaults
const CONFIG = {
model: process.env.MODEL || "claude-3-7-sonnet-20250219",
maxTokens: parseInt(process.env.MAX_TOKENS || "4000"),
temperature: parseFloat(process.env.TEMPERATURE || "0.7"),
model: MODEL,
maxTokens: MAX_TOKENS,
temperature: TEMPERATURE,
debug: process.env.DEBUG === "true",
logLevel: process.env.LOG_LEVEL || "info",
defaultSubtasks: parseInt(process.env.DEFAULT_SUBTASKS || "3"),
@@ -95,10 +114,6 @@ function log(level, ...args) {
}
}
const anthropic = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
});
function readJSON(filepath) {
if (!fs.existsSync(filepath)) return null;
const content = fs.readFileSync(filepath, 'utf8');
@@ -613,7 +628,7 @@ function setTaskStatus(tasksPath, taskIdInput, newStatus) {
//
// 5) list tasks
//
function listTasks(tasksPath) {
function listTasks(tasksPath, statusFilter, withSubtasks = false) {
const data = readJSON(tasksPath);
if (!data || !data.tasks) {
log('error', "No valid tasks found.");
@@ -621,114 +636,138 @@ function listTasks(tasksPath) {
}
log('info', `Tasks in ${tasksPath}:`);
data.tasks.forEach(t => {
// Filter tasks by status if a filter is provided
const filteredTasks = statusFilter
? data.tasks.filter(t => t.status === statusFilter)
: data.tasks;
filteredTasks.forEach(t => {
log('info', `- ID=${t.id}, [${t.status}] ${t.title}`);
// Display subtasks if requested and they exist
if (withSubtasks && t.subtasks && t.subtasks.length > 0) {
t.subtasks.forEach(st => {
log('info', ` └─ ID=${t.id}.${st.id}, [${st.status || 'pending'}] ${st.title}`);
});
}
});
// If no tasks match the filter, show a message
if (filteredTasks.length === 0) {
log('info', `No tasks found${statusFilter ? ` with status '${statusFilter}'` : ''}.`);
}
}
//
// 6) expand task with subtasks
//
async function expandTask(tasksPath, taskId, numSubtasks, additionalContext = '') {
const data = readJSON(tasksPath);
if (!data || !data.tasks) {
log('error', "No valid tasks found.");
process.exit(1);
/**
* Expand a task by generating subtasks
* @param {string} taskId - The ID of the task to expand
* @param {number} numSubtasks - The number of subtasks to generate
* @param {boolean} useResearch - Whether to use Perplexity for research-backed subtask generation
* @returns {Promise<void>}
*/
async function expandTask(taskId, numSubtasks = CONFIG.defaultSubtasks, useResearch = false) {
try {
// Get the tasks
const tasksData = readJSON(path.join(process.cwd(), 'tasks', 'tasks.json'));
const task = tasksData.tasks.find(t => t.id === parseInt(taskId));
if (!task) {
console.error(chalk.red(`Task with ID ${taskId} not found.`));
return;
}
// Check if the task is already completed
if (task.status === 'completed' || task.status === 'done') {
console.log(chalk.yellow(`Task ${taskId} is already completed. Skipping expansion.`));
return;
}
// Initialize subtasks array if it doesn't exist
if (!task.subtasks) {
task.subtasks = [];
}
// Calculate the next subtask ID
const nextSubtaskId = task.subtasks.length > 0
? Math.max(...task.subtasks.map(st => st.id)) + 1
: 1;
// Generate subtasks
let subtasks;
if (useResearch) {
console.log(chalk.blue(`Using Perplexity AI for research-backed subtask generation...`));
subtasks = await generateSubtasksWithPerplexity(task, numSubtasks, nextSubtaskId);
} else {
subtasks = await generateSubtasks(task, numSubtasks, nextSubtaskId);
}
// Add the subtasks to the task
task.subtasks = [...task.subtasks, ...subtasks];
// Save the updated tasks
fs.writeFileSync(
path.join(process.cwd(), 'tasks', 'tasks.json'),
JSON.stringify(tasksData, null, 2)
);
console.log(chalk.green(`Added ${subtasks.length} subtasks to task ${taskId}.`));
// Log the added subtasks
subtasks.forEach(st => {
console.log(chalk.cyan(` ${st.id}. ${st.title}`));
console.log(chalk.gray(` ${st.description.substring(0, 100)}${st.description.length > 100 ? '...' : ''}`));
});
} catch (error) {
console.error(chalk.red('Error expanding task:'), error);
}
// Use default subtasks count from config if not specified
numSubtasks = numSubtasks || CONFIG.defaultSubtasks;
const task = data.tasks.find(t => t.id === taskId);
if (!task) {
log('error', `Task with ID=${taskId} not found.`);
process.exit(1);
}
// Skip tasks that are already completed
if (task.status === 'done' || task.status === 'completed') {
log('info', `Skipping task ID=${taskId} "${task.title}" - task is already marked as ${task.status}.`);
log('info', `Use set-status command to change the status if you want to modify this task.`);
return false;
}
log('info', `Expanding task: ${task.title}`);
// Initialize subtasks array if it doesn't exist
if (!task.subtasks) {
task.subtasks = [];
}
// Calculate next subtask ID
const nextSubtaskId = task.subtasks.length > 0
? Math.max(...task.subtasks.map(st => st.id)) + 1
: 1;
// Generate subtasks using Claude
const subtasks = await generateSubtasks(task, numSubtasks, nextSubtaskId, additionalContext);
// Add new subtasks to the task
task.subtasks = [...task.subtasks, ...subtasks];
// Update tasks.json
writeJSON(tasksPath, data);
log('info', `Added ${subtasks.length} subtasks to task ID=${taskId}.`);
// Print the new subtasks
log('info', "New subtasks:");
subtasks.forEach(st => {
log('info', `- ${st.id}. ${st.title}`);
});
return true;
}
//
// Expand all tasks with subtasks
//
async function expandAllTasks(tasksPath, numSubtasks, additionalContext = '', forceRegenerate = false) {
const data = readJSON(tasksPath);
if (!data || !data.tasks) {
log('error', "No valid tasks found.");
process.exit(1);
}
log('info', `Expanding all ${data.tasks.length} tasks with subtasks...`);
let tasksExpanded = 0;
let tasksSkipped = 0;
let tasksCompleted = 0;
// Process each task sequentially to avoid overwhelming the API
for (const task of data.tasks) {
// Skip tasks that are already completed
if (task.status === 'done' || task.status === 'completed') {
log('info', `Skipping task ID=${task.id} "${task.title}" - task is already marked as ${task.status}.`);
tasksCompleted++;
continue;
/**
* Expand all tasks that are not completed
* @param {number} numSubtasks - The number of subtasks to generate for each task
* @param {boolean} useResearch - Whether to use Perplexity for research-backed subtask generation
* @returns {Promise<number>} - The number of tasks expanded
*/
async function expandAllTasks(numSubtasks = CONFIG.defaultSubtasks, useResearch = false) {
try {
// Get the tasks
const tasksData = readJSON(path.join(process.cwd(), 'tasks', 'tasks.json'));
if (!tasksData || !tasksData.tasks || !Array.isArray(tasksData.tasks)) {
console.error(chalk.red('No valid tasks found.'));
return 0;
}
// Skip tasks that already have subtasks unless force regeneration is enabled
if (!forceRegenerate && task.subtasks && task.subtasks.length > 0) {
log('info', `Skipping task ID=${task.id} "${task.title}" - already has ${task.subtasks.length} subtasks`);
tasksSkipped++;
continue;
// Filter tasks that are not completed
const tasksToExpand = tasksData.tasks.filter(task =>
task.status !== 'completed' && task.status !== 'done'
);
if (tasksToExpand.length === 0) {
console.log(chalk.yellow('No tasks to expand. All tasks are already completed.'));
return 0;
}
const success = await expandTask(tasksPath, task.id, numSubtasks, additionalContext);
if (success) {
console.log(chalk.blue(`Expanding ${tasksToExpand.length} tasks with ${numSubtasks} subtasks each...`));
let tasksExpanded = 0;
// Expand each task
for (const task of tasksToExpand) {
console.log(chalk.blue(`\nExpanding task ${task.id}: ${task.title}`));
await expandTask(task.id, numSubtasks, useResearch);
tasksExpanded++;
}
}
log('info', `Expansion complete: ${tasksExpanded} tasks expanded, ${tasksSkipped} tasks skipped (already had subtasks), ${tasksCompleted} tasks skipped (already completed).`);
if (tasksSkipped > 0) {
log('info', `Tip: Use --force flag to regenerate subtasks for all tasks, including those that already have subtasks.`);
}
if (tasksCompleted > 0) {
log('info', `Note: Completed tasks are always skipped. Use set-status command to change task status if needed.`);
console.log(chalk.green(`\nExpanded ${tasksExpanded} tasks with ${numSubtasks} subtasks each.`));
return tasksExpanded;
} catch (error) {
console.error(chalk.red('Error expanding all tasks:'), error);
return 0;
}
}
@@ -961,135 +1000,272 @@ function parseSubtasksFromText(text, startId, expectedCount) {
return subtasks;
}
/**
* Generate subtasks for a task using Perplexity AI with research capabilities
* @param {Object} task - The task to generate subtasks for
* @param {number} numSubtasks - The number of subtasks to generate
* @param {number} nextSubtaskId - The ID to start assigning to subtasks
* @returns {Promise<Array>} - The generated subtasks
*/
async function generateSubtasksWithPerplexity(task, numSubtasks = 3, nextSubtaskId = 1) {
const { title, description, details = '', subtasks = [] } = task;
console.log(chalk.blue(`Generating ${numSubtasks} subtasks for task: ${title}`));
if (subtasks.length > 0) {
console.log(chalk.yellow(`Task already has ${subtasks.length} subtasks. Adding ${numSubtasks} more.`));
}
// Get the tasks.json content for context
let tasksData = {};
try {
tasksData = readJSON(path.join(process.cwd(), 'tasks', 'tasks.json'));
} catch (error) {
console.log(chalk.yellow('Could not read tasks.json for context. Proceeding without it.'));
}
// Get the PRD content for context if available
let prdContent = '';
if (tasksData.meta && tasksData.meta.source) {
try {
prdContent = fs.readFileSync(path.join(process.cwd(), tasksData.meta.source), 'utf8');
} catch (error) {
console.log(chalk.yellow(`Could not read PRD at ${tasksData.meta.source}. Proceeding without it.`));
}
}
// Construct the prompt for Perplexity/Anthropic
const prompt = `I need to break down the following task into ${numSubtasks} detailed subtasks:
Task Title: ${title}
Task Description: ${description}
Additional Details: ${details}
${subtasks.length > 0 ? `Existing Subtasks:
${subtasks.map(st => `- ${st.title}: ${st.description}`).join('\n')}` : ''}
${prdContent ? `Here is the Product Requirements Document for context:
${prdContent}` : ''}
${tasksData.tasks ? `Here are the other tasks in the project for context:
${JSON.stringify(tasksData.tasks.filter(t => t.id !== task.id).map(t => ({ id: t.id, title: t.title, description: t.description })), null, 2)}` : ''}
Please generate ${numSubtasks} subtasks. For each subtask, provide:
1. A clear, concise title
2. A detailed description explaining what needs to be done
3. Dependencies (if any) - list the IDs of tasks this subtask depends on
4. Acceptance criteria - specific conditions that must be met for the subtask to be considered complete
Format each subtask as follows:
Subtask 1: [Title]
Description: [Detailed description]
Dependencies: [List of task IDs, or "None" if no dependencies]
Acceptance Criteria: [List of criteria]
Subtask 2: [Title]
...
Research the task thoroughly and ensure the subtasks are comprehensive, specific, and actionable.`;
// Start loading indicator
const loadingInterval = startLoadingIndicator('Researching and generating subtasks with AI');
try {
let responseText;
try {
// Try to use Perplexity first
console.log(chalk.blue('Using Perplexity AI for research-backed subtask generation...'));
const result = await perplexity.chat.completions.create({
model: PERPLEXITY_MODEL,
messages: [{
role: "user",
content: prompt
}],
temperature: TEMPERATURE,
max_tokens: MAX_TOKENS,
});
// Extract the response text
responseText = result.choices[0].message.content;
console.log(chalk.green('Successfully generated subtasks with Perplexity AI'));
} catch (perplexityError) {
console.log(chalk.yellow('Falling back to Anthropic for subtask generation...'));
console.log(chalk.gray('Perplexity error:'), perplexityError.message);
// Use Anthropic as fallback
const stream = await anthropic.messages.create({
model: MODEL,
max_tokens: MAX_TOKENS,
temperature: TEMPERATURE,
system: "You are an expert software developer and project manager. Your task is to break down software development tasks into detailed subtasks.",
messages: [
{
role: "user",
content: prompt
}
],
stream: true
});
// Process the stream
responseText = '';
for await (const chunk of stream) {
if (chunk.type === 'content_block_delta' && chunk.delta.text) {
responseText += chunk.delta.text;
}
}
console.log(chalk.green('Successfully generated subtasks with Anthropic AI'));
}
// Stop loading indicator
stopLoadingIndicator(loadingInterval);
if (CONFIG.debug) {
console.log(chalk.gray('AI Response:'));
console.log(chalk.gray(responseText));
}
// Parse the subtasks from the response text
const subtasks = parseSubtasksFromText(responseText, nextSubtaskId, numSubtasks);
return subtasks;
} catch (error) {
stopLoadingIndicator(loadingInterval);
console.error(chalk.red('Error generating subtasks:'), error);
throw error;
}
}
// ------------------------------------------
// Main CLI
// ------------------------------------------
(async function main() {
const args = process.argv.slice(2);
const command = args[0];
async function main() {
program
.name('dev')
.description('AI-driven development task management')
.version('1.3.1');
const outputDir = path.resolve(process.cwd(), 'tasks');
// Update tasksPath to be inside the tasks directory
const tasksPath = path.resolve(outputDir, 'tasks.json');
program
.command('parse-prd')
.description('Parse a PRD file and generate tasks')
.argument('<file>', 'Path to the PRD file')
.option('-o, --output <file>', 'Output file path', 'tasks/tasks.json')
.option('-n, --num-tasks <number>', 'Number of tasks to generate', '10')
.action(async (file, options) => {
const numTasks = parseInt(options.numTasks, 10);
const outputPath = options.output;
console.log(chalk.blue(`Parsing PRD file: ${file}`));
console.log(chalk.blue(`Generating ${numTasks} tasks...`));
await parsePRD(file, outputPath, numTasks);
});
const inputArg = (args.find(a => a.startsWith('--input=')) || '').split('=')[1] || 'sample-prd.txt';
const fromArg = (args.find(a => a.startsWith('--from=')) || '').split('=')[1];
const promptArg = (args.find(a => a.startsWith('--prompt=')) || '').split('=')[1] || '';
const idArg = (args.find(a => a.startsWith('--id=')) || '').split('=')[1];
const statusArg = (args.find(a => a.startsWith('--status=')) || '').split('=')[1] || '';
const tasksCountArg = (args.find(a => a.startsWith('--tasks=')) || '').split('=')[1];
const numTasks = tasksCountArg ? parseInt(tasksCountArg, 10) : undefined;
const subtasksArg = (args.find(a => a.startsWith('--subtasks=')) || '').split('=')[1];
const numSubtasks = subtasksArg ? parseInt(subtasksArg, 10) : 3; // Default to 3 subtasks if not specified
const forceFlag = args.includes('--force'); // Check if --force flag is present
program
.command('update')
.description('Update tasks based on the PRD')
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.action(async (options) => {
const tasksPath = options.file;
console.log(chalk.blue(`Updating tasks from: ${tasksPath}`));
await updateTasks(tasksPath);
});
log('info', `Executing command: ${command}`);
// Make sure the tasks directory exists
if (!fs.existsSync(outputDir)) {
log('info', `Creating tasks directory: ${outputDir}`);
fs.mkdirSync(outputDir, { recursive: true });
}
switch (command) {
case 'parse-prd':
log('info', `Parsing PRD from ${inputArg} to generate tasks.json...`);
if (numTasks) {
log('info', `Limiting to ${numTasks} tasks as specified`);
program
.command('generate')
.description('Generate task files from tasks.json')
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.option('-o, --output <dir>', 'Output directory', 'tasks')
.action(async (options) => {
const tasksPath = options.file;
const outputDir = options.output;
console.log(chalk.blue(`Generating task files from: ${tasksPath}`));
console.log(chalk.blue(`Output directory: ${outputDir}`));
await generateTaskFiles(tasksPath, outputDir);
});
program
.command('set-status')
.description('Set the status of a task')
.argument('<id>', 'Task ID')
.argument('<status>', 'New status (todo, in-progress, review, done)')
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.action(async (id, status, options) => {
const tasksPath = options.file;
const taskId = parseInt(id, 10);
console.log(chalk.blue(`Setting status of task ${taskId} to: ${status}`));
await setTaskStatus(tasksPath, taskId, status);
});
program
.command('list')
.description('List all tasks')
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.option('-s, --status <status>', 'Filter by status')
.option('--with-subtasks', 'Show subtasks for each task')
.action(async (options) => {
const tasksPath = options.file;
const statusFilter = options.status;
const withSubtasks = options.withSubtasks || false;
console.log(chalk.blue(`Listing tasks from: ${tasksPath}`));
if (statusFilter) {
console.log(chalk.blue(`Filtering by status: ${statusFilter}`));
}
await parsePRD(inputArg, tasksPath, numTasks);
break;
case 'update':
if (!fromArg) {
log('error', "Please specify --from=<id>. e.g. node dev.js update --from=3 --prompt='Changes...'");
process.exit(1);
if (withSubtasks) {
console.log(chalk.blue('Including subtasks in listing'));
}
log('info', `Updating tasks from ID ${fromArg} based on prompt...`);
await updateTasks(tasksPath, parseInt(fromArg, 10), promptArg);
break;
await listTasks(tasksPath, statusFilter, withSubtasks);
});
case 'generate':
log('info', `Generating individual task files from ${tasksPath} to ${outputDir}...`);
generateTaskFiles(tasksPath, outputDir);
break;
case 'set-status':
if (!idArg) {
log('error', "Missing --id=<taskId> argument.");
process.exit(1);
}
if (!statusArg) {
log('error', "Missing --status=<newStatus> argument (e.g., done, pending, deferred, in-progress).");
process.exit(1);
}
log('info', `Setting task(s) ${idArg} status to "${statusArg}"...`);
setTaskStatus(tasksPath, idArg, statusArg);
break;
case 'list':
log('info', `Listing tasks from ${tasksPath}...`);
listTasks(tasksPath);
break;
case 'expand':
if (args.includes('--all')) {
// Expand all tasks
log('info', `Expanding all tasks with ${numSubtasks} subtasks each...`);
await expandAllTasks(tasksPath, numSubtasks, promptArg, forceFlag);
program
.command('expand')
.description('Expand tasks with subtasks')
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.option('-i, --id <id>', 'Task ID to expand')
.option('-a, --all', 'Expand all tasks')
.option('-n, --num <number>', 'Number of subtasks to generate', CONFIG.defaultSubtasks.toString())
.option('-r, --research', 'Use Perplexity AI for research-backed subtask generation')
.option('--force', 'Force regeneration of subtasks for tasks that already have them')
.action(async (options) => {
const tasksPath = options.file;
const idArg = options.id ? parseInt(options.id, 10) : null;
const allFlag = options.all;
const numSubtasks = parseInt(options.num, 10);
const forceFlag = options.force;
const useResearch = options.research;
if (allFlag) {
console.log(chalk.blue(`Expanding all tasks with ${numSubtasks} subtasks each...`));
if (useResearch) {
console.log(chalk.blue('Using Perplexity AI for research-backed subtask generation'));
}
await expandAllTasks(numSubtasks, useResearch);
} else if (idArg) {
// Expand a specific task
log('info', `Expanding task ${idArg} with ${numSubtasks} subtasks...`);
await expandTask(tasksPath, parseInt(idArg, 10), numSubtasks, promptArg);
console.log(chalk.blue(`Expanding task ${idArg} with ${numSubtasks} subtasks...`));
if (useResearch) {
console.log(chalk.blue('Using Perplexity AI for research-backed subtask generation'));
}
await expandTask(idArg, numSubtasks, useResearch);
} else {
log('error', "Error: Please specify a task ID with --id=<id> or use --all to expand all tasks.");
process.exit(1);
console.error(chalk.red('Error: Please specify a task ID with --id=<id> or use --all to expand all tasks.'));
}
break;
});
default:
log('info', `
Dev.js - Task Management Script
await program.parseAsync(process.argv);
}
Subcommands:
1) parse-prd --input=some-prd.txt [--tasks=10]
-> Creates/overwrites tasks.json with a set of tasks.
-> Optional --tasks parameter limits the number of tasks generated.
// ... existing code ...
2) update --from=5 --prompt="We changed from Slack to Discord."
-> Regenerates tasks from ID >= 5 using the provided prompt.
3) generate
-> Generates per-task files (e.g., task_001.txt) from tasks.json
4) set-status --id=4 --status=done
-> Updates a single task's status to done (or pending, deferred, in-progress, etc.).
-> Supports comma-separated IDs for updating multiple tasks: --id=1,2,3,1.1,1.2
5) list
-> Lists tasks in a brief console view (ID, title, status).
6) expand --id=3 --subtasks=5 [--prompt="Additional context"]
-> Expands a task with subtasks for more detailed implementation.
-> Use --all instead of --id to expand all tasks.
-> Optional --subtasks parameter controls number of subtasks (default: 3).
-> Add --force when using --all to regenerate subtasks for tasks that already have them.
-> Note: Tasks marked as 'done' or 'completed' are always skipped.
Usage examples:
node dev.js parse-prd --input=scripts/prd.txt
node dev.js parse-prd --input=scripts/prd.txt --tasks=10
node dev.js update --from=4 --prompt="Refactor tasks from ID 4 onward"
node dev.js generate
node dev.js set-status --id=3 --status=done
node dev.js list
node dev.js expand --id=3 --subtasks=5
node dev.js expand --all
node dev.js expand --all --force
`);
break;
}
})().catch(err => {
main().catch(err => {
log('error', err);
process.exit(1);
});