Compare commits
31 Commits
feat/add.g
...
task-maste
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4f92f6a0a | ||
|
|
be0c0f267c | ||
|
|
a983f75d4f | ||
|
|
e743aaa8c2 | ||
|
|
16ffffaf68 | ||
|
|
f254aed4a6 | ||
|
|
dd3b47bb2b | ||
|
|
37af0f1912 | ||
|
|
8783708e5e | ||
|
|
4dad2fd613 | ||
|
|
4cae2991d4 | ||
|
|
0d7ff627c9 | ||
|
|
db720a954d | ||
|
|
89335578ff | ||
|
|
781b8ef2af | ||
|
|
7d564920b5 | ||
|
|
2737fbaa67 | ||
|
|
9feb8d2dbf | ||
|
|
8a991587f1 | ||
|
|
7ceba2f572 | ||
|
|
10565f07d3 | ||
|
|
f27ce34fe9 | ||
|
|
71be933a8d | ||
|
|
5d94f1b471 | ||
|
|
3dee60dc3d | ||
|
|
f469515228 | ||
|
|
2fd0f026d3 | ||
|
|
e3ed4d7c14 | ||
|
|
fc47714340 | ||
|
|
30ae0e9a57 | ||
|
|
95640dcde8 |
5
.changeset/clarify-force-move-docs.md
Normal file
5
.changeset/clarify-force-move-docs.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"task-master-ai": patch
|
||||
---
|
||||
|
||||
docs(move): clarify cross-tag move docs; deprecate "force"; add explicit --with-dependencies/--ignore-dependencies examples
|
||||
@@ -1,27 +0,0 @@
|
||||
---
|
||||
"task-master-ai": minor
|
||||
---
|
||||
|
||||
Add cross-tag task movement functionality for organizing tasks across different contexts.
|
||||
|
||||
This feature enables moving tasks between different tags (contexts) in your project, making it easier to organize work across different branches, environments, or project phases.
|
||||
|
||||
## CLI Usage Examples
|
||||
|
||||
Move a single task from one tag to another:
|
||||
```bash
|
||||
# Move task 5 from backlog tag to in-progress tag
|
||||
task-master move --from=5 --from-tag=backlog --to-tag=feature-1
|
||||
|
||||
# Move task with its dependencies
|
||||
task-master move --from=5 --from-tag=backlog --to-tag=feature-2 --with-dependencies
|
||||
|
||||
# Move task without checking dependencies
|
||||
task-master move --from=5 --from-tag=backlog --to-tag=bug-3 --ignore-dependencies
|
||||
```
|
||||
|
||||
Move multiple tasks at once:
|
||||
```bash
|
||||
# Move multiple tasks between tags
|
||||
task-master move --from=5,6,7 --from-tag=backlog --to-tag=bug-4 --with-dependencies
|
||||
```
|
||||
9
.changeset/curvy-moons-dig.md
Normal file
9
.changeset/curvy-moons-dig.md
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
"task-master-ai": minor
|
||||
---
|
||||
|
||||
Enhanced Gemini CLI provider with codebase-aware task generation
|
||||
|
||||
Added automatic codebase analysis for Gemini CLI provider in parse-prd, and analyze-complexity, add-task, udpate-task, update, update-subtask commands
|
||||
When using Gemini CLI as the AI provider, Task Master now instructs the AI to analyze the project structure, existing implementations, and patterns before generating tasks or subtasks
|
||||
Tasks and subtasks generated by Claude Code are now informed by actual codebase analysis, resulting in more accurate and contextual outputs
|
||||
@@ -1,12 +0,0 @@
|
||||
---
|
||||
"task-master-ai": minor
|
||||
---
|
||||
|
||||
Add compact mode --compact / -c flag to the `tm list` CLI command
|
||||
|
||||
- outputs tasks in a minimal, git-style one-line format. This reduces verbose output from ~30+ lines of dashboards and tables to just 1 line per task, making it much easier to quickly scan available tasks.
|
||||
- Git-style format: ID STATUS TITLE (PRIORITY) → DEPS
|
||||
- Color-coded status, priority, and dependencies
|
||||
- Smart title truncation and dependency abbreviation
|
||||
- Subtask support with indentation
|
||||
- Full backward compatibility with existing list options
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"extension": minor
|
||||
---
|
||||
|
||||
Display current task ID on task details page
|
||||
15
.changeset/pre.json
Normal file
15
.changeset/pre.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"mode": "pre",
|
||||
"tag": "rc",
|
||||
"initialVersions": {
|
||||
"task-master-ai": "0.25.1",
|
||||
"docs": "0.0.1",
|
||||
"extension": "0.24.1"
|
||||
},
|
||||
"changesets": [
|
||||
"clarify-force-move-docs",
|
||||
"curvy-moons-dig",
|
||||
"strong-eagles-vanish",
|
||||
"wet-candies-accept"
|
||||
]
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
"task-master-ai": patch
|
||||
---
|
||||
|
||||
Fix `add-tag --from-branch` command error where `projectRoot` was not properly referenced
|
||||
|
||||
The command was failing with "projectRoot is not defined" error because the code was directly referencing `projectRoot` instead of `context.projectRoot` in the git repository checks. This fix corrects the variable references to use the proper context object.
|
||||
11
.changeset/sour-coins-lay.md
Normal file
11
.changeset/sour-coins-lay.md
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
"task-master-ai": minor
|
||||
---
|
||||
|
||||
Add configurable codebase analysis feature flag with multiple configuration sources
|
||||
|
||||
Users can now control whether codebase analysis features (Claude Code and Gemini CLI integration) are enabled through environment variables, MCP configuration, or project config files.
|
||||
|
||||
Priority order: .env > MCP session env > .taskmaster/config.json.
|
||||
|
||||
Set `TASKMASTER_ENABLE_CODEBASE_ANALYSIS=false` in `.env` to disable codebase analysis prompts and tool integration.
|
||||
12
.changeset/strong-eagles-vanish.md
Normal file
12
.changeset/strong-eagles-vanish.md
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
"task-master-ai": minor
|
||||
---
|
||||
|
||||
feat(move): improve cross-tag move UX and safety
|
||||
|
||||
- CLI: print "Next Steps" tips after cross-tag moves that used --ignore-dependencies (validate/fix guidance)
|
||||
- CLI: show dedicated help block on ID collisions (destination tag already has the ID)
|
||||
- Core: add structured suggestions to TASK_ALREADY_EXISTS errors
|
||||
- MCP: map ID collision errors to TASK_ALREADY_EXISTS and include suggestions
|
||||
- Tests: cover MCP options, error suggestions, CLI tips printing, and integration error payload suggestions
|
||||
---
|
||||
14
.changeset/wet-candies-accept.md
Normal file
14
.changeset/wet-candies-accept.md
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
"task-master-ai": minor
|
||||
---
|
||||
|
||||
Enhanced Claude Code and Google CLI integration with automatic codebase analysis for task operations
|
||||
|
||||
When using Claude Code as the AI provider, task management commands now automatically analyze your codebase before generating or updating tasks. This provides more accurate, context-aware implementation details that align with your project's existing architecture and patterns.
|
||||
|
||||
Commands contextualised:
|
||||
|
||||
- add-task
|
||||
- update-subtask
|
||||
- update-task
|
||||
- update
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"task-master-ai": minor
|
||||
---
|
||||
|
||||
Remove `clear` Taskmaster claude code commands since they were too close to the claude-code clear command
|
||||
38
.claude/commands/dedupe.md
Normal file
38
.claude/commands/dedupe.md
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
allowed-tools: Bash(gh issue view:*), Bash(gh search:*), Bash(gh issue list:*), Bash(gh api:*), Bash(gh issue comment:*)
|
||||
description: Find duplicate GitHub issues
|
||||
---
|
||||
|
||||
Find up to 3 likely duplicate issues for a given GitHub issue.
|
||||
|
||||
To do this, follow these steps precisely:
|
||||
|
||||
1. Use an agent to check if the Github issue (a) is closed, (b) does not need to be deduped (eg. because it is broad product feedback without a specific solution, or positive feedback), or (c) already has a duplicates comment that you made earlier. If so, do not proceed.
|
||||
2. Use an agent to view a Github issue, and ask the agent to return a summary of the issue
|
||||
3. Then, launch 5 parallel agents to search Github for duplicates of this issue, using diverse keywords and search approaches, using the summary from #1
|
||||
4. Next, feed the results from #1 and #2 into another agent, so that it can filter out false positives, that are likely not actually duplicates of the original issue. If there are no duplicates remaining, do not proceed.
|
||||
5. Finally, comment back on the issue with a list of up to three duplicate issues (or zero, if there are no likely duplicates)
|
||||
|
||||
Notes (be sure to tell this to your agents, too):
|
||||
|
||||
- Use `gh` to interact with Github, rather than web fetch
|
||||
- Do not use other tools, beyond `gh` (eg. don't use other MCP servers, file edit, etc.)
|
||||
- Make a todo list first
|
||||
- For your comment, follow the following format precisely (assuming for this example that you found 3 suspected duplicates):
|
||||
|
||||
---
|
||||
|
||||
Found 3 possible duplicate issues:
|
||||
|
||||
1. <link to issue>
|
||||
2. <link to issue>
|
||||
3. <link to issue>
|
||||
|
||||
This issue will be automatically closed as a duplicate in 3 days.
|
||||
|
||||
- If your issue is a duplicate, please close it and 👍 the existing issue instead
|
||||
- To prevent auto-closure, add a comment or 👎 this comment
|
||||
|
||||
🤖 Generated with \[Task Master Bot\]
|
||||
|
||||
---
|
||||
259
.github/scripts/auto-close-duplicates.mjs
vendored
Normal file
259
.github/scripts/auto-close-duplicates.mjs
vendored
Normal file
@@ -0,0 +1,259 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
async function githubRequest(endpoint, token, method = 'GET', body) {
|
||||
const response = await fetch(`https://api.github.com${endpoint}`, {
|
||||
method,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
Accept: 'application/vnd.github.v3+json',
|
||||
'User-Agent': 'auto-close-duplicates-script',
|
||||
...(body && { 'Content-Type': 'application/json' })
|
||||
},
|
||||
...(body && { body: JSON.stringify(body) })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`GitHub API request failed: ${response.status} ${response.statusText}`
|
||||
);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
function extractDuplicateIssueNumber(commentBody) {
|
||||
const match = commentBody.match(/#(\d+)/);
|
||||
return match ? parseInt(match[1], 10) : null;
|
||||
}
|
||||
|
||||
async function closeIssueAsDuplicate(
|
||||
owner,
|
||||
repo,
|
||||
issueNumber,
|
||||
duplicateOfNumber,
|
||||
token
|
||||
) {
|
||||
await githubRequest(
|
||||
`/repos/${owner}/${repo}/issues/${issueNumber}`,
|
||||
token,
|
||||
'PATCH',
|
||||
{
|
||||
state: 'closed',
|
||||
state_reason: 'not_planned',
|
||||
labels: ['duplicate']
|
||||
}
|
||||
);
|
||||
|
||||
await githubRequest(
|
||||
`/repos/${owner}/${repo}/issues/${issueNumber}/comments`,
|
||||
token,
|
||||
'POST',
|
||||
{
|
||||
body: `This issue has been automatically closed as a duplicate of #${duplicateOfNumber}.
|
||||
|
||||
If this is incorrect, please re-open this issue or create a new one.
|
||||
|
||||
🤖 Generated with [Task Master Bot]`
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function autoCloseDuplicates() {
|
||||
console.log('[DEBUG] Starting auto-close duplicates script');
|
||||
|
||||
const token = process.env.GITHUB_TOKEN;
|
||||
if (!token) {
|
||||
throw new Error('GITHUB_TOKEN environment variable is required');
|
||||
}
|
||||
console.log('[DEBUG] GitHub token found');
|
||||
|
||||
const owner = process.env.GITHUB_REPOSITORY_OWNER || 'eyaltoledano';
|
||||
const repo = process.env.GITHUB_REPOSITORY_NAME || 'claude-task-master';
|
||||
console.log(`[DEBUG] Repository: ${owner}/${repo}`);
|
||||
|
||||
const threeDaysAgo = new Date();
|
||||
threeDaysAgo.setDate(threeDaysAgo.getDate() - 3);
|
||||
console.log(
|
||||
`[DEBUG] Checking for duplicate comments older than: ${threeDaysAgo.toISOString()}`
|
||||
);
|
||||
|
||||
console.log('[DEBUG] Fetching open issues created more than 3 days ago...');
|
||||
const allIssues = [];
|
||||
let page = 1;
|
||||
const perPage = 100;
|
||||
|
||||
const MAX_PAGES = 50; // Increase limit for larger repos
|
||||
let foundRecentIssue = false;
|
||||
|
||||
while (true) {
|
||||
const pageIssues = await githubRequest(
|
||||
`/repos/${owner}/${repo}/issues?state=open&per_page=${perPage}&page=${page}&sort=created&direction=desc`,
|
||||
token
|
||||
);
|
||||
|
||||
if (pageIssues.length === 0) break;
|
||||
|
||||
// Filter for issues created more than 3 days ago
|
||||
const oldEnoughIssues = pageIssues.filter(
|
||||
(issue) => new Date(issue.created_at) <= threeDaysAgo
|
||||
);
|
||||
|
||||
allIssues.push(...oldEnoughIssues);
|
||||
|
||||
// If all issues on this page are newer than 3 days, we can stop
|
||||
if (oldEnoughIssues.length === 0 && page === 1) {
|
||||
foundRecentIssue = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// If we found some old issues but not all, continue to next page
|
||||
// as there might be more old issues
|
||||
page++;
|
||||
|
||||
// Safety limit to avoid infinite loops
|
||||
if (page > MAX_PAGES) {
|
||||
console.log(`[WARNING] Reached maximum page limit of ${MAX_PAGES}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const issues = allIssues;
|
||||
console.log(`[DEBUG] Found ${issues.length} open issues`);
|
||||
|
||||
let processedCount = 0;
|
||||
let candidateCount = 0;
|
||||
|
||||
for (const issue of issues) {
|
||||
processedCount++;
|
||||
console.log(
|
||||
`[DEBUG] Processing issue #${issue.number} (${processedCount}/${issues.length}): ${issue.title}`
|
||||
);
|
||||
|
||||
console.log(`[DEBUG] Fetching comments for issue #${issue.number}...`);
|
||||
const comments = await githubRequest(
|
||||
`/repos/${owner}/${repo}/issues/${issue.number}/comments`,
|
||||
token
|
||||
);
|
||||
console.log(
|
||||
`[DEBUG] Issue #${issue.number} has ${comments.length} comments`
|
||||
);
|
||||
|
||||
const dupeComments = comments.filter(
|
||||
(comment) =>
|
||||
comment.body.includes('Found') &&
|
||||
comment.body.includes('possible duplicate') &&
|
||||
comment.user.type === 'Bot'
|
||||
);
|
||||
console.log(
|
||||
`[DEBUG] Issue #${issue.number} has ${dupeComments.length} duplicate detection comments`
|
||||
);
|
||||
|
||||
if (dupeComments.length === 0) {
|
||||
console.log(
|
||||
`[DEBUG] Issue #${issue.number} - no duplicate comments found, skipping`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const lastDupeComment = dupeComments[dupeComments.length - 1];
|
||||
const dupeCommentDate = new Date(lastDupeComment.created_at);
|
||||
console.log(
|
||||
`[DEBUG] Issue #${
|
||||
issue.number
|
||||
} - most recent duplicate comment from: ${dupeCommentDate.toISOString()}`
|
||||
);
|
||||
|
||||
if (dupeCommentDate > threeDaysAgo) {
|
||||
console.log(
|
||||
`[DEBUG] Issue #${issue.number} - duplicate comment is too recent, skipping`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
console.log(
|
||||
`[DEBUG] Issue #${
|
||||
issue.number
|
||||
} - duplicate comment is old enough (${Math.floor(
|
||||
(Date.now() - dupeCommentDate.getTime()) / (1000 * 60 * 60 * 24)
|
||||
)} days)`
|
||||
);
|
||||
|
||||
const commentsAfterDupe = comments.filter(
|
||||
(comment) => new Date(comment.created_at) > dupeCommentDate
|
||||
);
|
||||
console.log(
|
||||
`[DEBUG] Issue #${issue.number} - ${commentsAfterDupe.length} comments after duplicate detection`
|
||||
);
|
||||
|
||||
if (commentsAfterDupe.length > 0) {
|
||||
console.log(
|
||||
`[DEBUG] Issue #${issue.number} - has activity after duplicate comment, skipping`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[DEBUG] Issue #${issue.number} - checking reactions on duplicate comment...`
|
||||
);
|
||||
const reactions = await githubRequest(
|
||||
`/repos/${owner}/${repo}/issues/comments/${lastDupeComment.id}/reactions`,
|
||||
token
|
||||
);
|
||||
console.log(
|
||||
`[DEBUG] Issue #${issue.number} - duplicate comment has ${reactions.length} reactions`
|
||||
);
|
||||
|
||||
const authorThumbsDown = reactions.some(
|
||||
(reaction) =>
|
||||
reaction.user.id === issue.user.id && reaction.content === '-1'
|
||||
);
|
||||
console.log(
|
||||
`[DEBUG] Issue #${issue.number} - author thumbs down reaction: ${authorThumbsDown}`
|
||||
);
|
||||
|
||||
if (authorThumbsDown) {
|
||||
console.log(
|
||||
`[DEBUG] Issue #${issue.number} - author disagreed with duplicate detection, skipping`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const duplicateIssueNumber = extractDuplicateIssueNumber(
|
||||
lastDupeComment.body
|
||||
);
|
||||
if (!duplicateIssueNumber) {
|
||||
console.log(
|
||||
`[DEBUG] Issue #${issue.number} - could not extract duplicate issue number from comment, skipping`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
candidateCount++;
|
||||
const issueUrl = `https://github.com/${owner}/${repo}/issues/${issue.number}`;
|
||||
|
||||
try {
|
||||
console.log(
|
||||
`[INFO] Auto-closing issue #${issue.number} as duplicate of #${duplicateIssueNumber}: ${issueUrl}`
|
||||
);
|
||||
await closeIssueAsDuplicate(
|
||||
owner,
|
||||
repo,
|
||||
issue.number,
|
||||
duplicateIssueNumber,
|
||||
token
|
||||
);
|
||||
console.log(
|
||||
`[SUCCESS] Successfully closed issue #${issue.number} as duplicate of #${duplicateIssueNumber}`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[ERROR] Failed to close issue #${issue.number} as duplicate: ${error}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[DEBUG] Script completed. Processed ${processedCount} issues, found ${candidateCount} candidates for auto-close`
|
||||
);
|
||||
}
|
||||
|
||||
autoCloseDuplicates().catch(console.error);
|
||||
178
.github/scripts/backfill-duplicate-comments.mjs
vendored
Normal file
178
.github/scripts/backfill-duplicate-comments.mjs
vendored
Normal file
@@ -0,0 +1,178 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
async function githubRequest(endpoint, token, method = 'GET', body) {
|
||||
const response = await fetch(`https://api.github.com${endpoint}`, {
|
||||
method,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
Accept: 'application/vnd.github.v3+json',
|
||||
'User-Agent': 'backfill-duplicate-comments-script',
|
||||
...(body && { 'Content-Type': 'application/json' })
|
||||
},
|
||||
...(body && { body: JSON.stringify(body) })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`GitHub API request failed: ${response.status} ${response.statusText}`
|
||||
);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async function triggerDedupeWorkflow(
|
||||
owner,
|
||||
repo,
|
||||
issueNumber,
|
||||
token,
|
||||
dryRun = true
|
||||
) {
|
||||
if (dryRun) {
|
||||
console.log(
|
||||
`[DRY RUN] Would trigger dedupe workflow for issue #${issueNumber}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await githubRequest(
|
||||
`/repos/${owner}/${repo}/actions/workflows/claude-dedupe-issues.yml/dispatches`,
|
||||
token,
|
||||
'POST',
|
||||
{
|
||||
ref: 'main',
|
||||
inputs: {
|
||||
issue_number: issueNumber.toString()
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function backfillDuplicateComments() {
|
||||
console.log('[DEBUG] Starting backfill duplicate comments script');
|
||||
|
||||
const token = process.env.GITHUB_TOKEN;
|
||||
if (!token) {
|
||||
throw new Error(`GITHUB_TOKEN environment variable is required
|
||||
|
||||
Usage:
|
||||
node .github/scripts/backfill-duplicate-comments.mjs
|
||||
|
||||
Environment Variables:
|
||||
GITHUB_TOKEN - GitHub personal access token with repo and actions permissions (required)
|
||||
DRY_RUN - Set to "false" to actually trigger workflows (default: true for safety)
|
||||
DAYS_BACK - How many days back to look for old issues (default: 90)`);
|
||||
}
|
||||
console.log('[DEBUG] GitHub token found');
|
||||
|
||||
const owner = process.env.GITHUB_REPOSITORY_OWNER || 'eyaltoledano';
|
||||
const repo = process.env.GITHUB_REPOSITORY_NAME || 'claude-task-master';
|
||||
const dryRun = process.env.DRY_RUN !== 'false';
|
||||
const daysBack = parseInt(process.env.DAYS_BACK || '90', 10);
|
||||
|
||||
console.log(`[DEBUG] Repository: ${owner}/${repo}`);
|
||||
console.log(`[DEBUG] Dry run mode: ${dryRun}`);
|
||||
console.log(`[DEBUG] Looking back ${daysBack} days`);
|
||||
|
||||
const cutoffDate = new Date();
|
||||
cutoffDate.setDate(cutoffDate.getDate() - daysBack);
|
||||
|
||||
console.log(
|
||||
`[DEBUG] Fetching issues created since ${cutoffDate.toISOString()}...`
|
||||
);
|
||||
const allIssues = [];
|
||||
let page = 1;
|
||||
const perPage = 100;
|
||||
|
||||
while (true) {
|
||||
const pageIssues = await githubRequest(
|
||||
`/repos/${owner}/${repo}/issues?state=all&per_page=${perPage}&page=${page}&since=${cutoffDate.toISOString()}`,
|
||||
token
|
||||
);
|
||||
|
||||
if (pageIssues.length === 0) break;
|
||||
|
||||
allIssues.push(...pageIssues);
|
||||
page++;
|
||||
|
||||
// Safety limit to avoid infinite loops
|
||||
if (page > 100) {
|
||||
console.log('[DEBUG] Reached page limit, stopping pagination');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[DEBUG] Found ${allIssues.length} issues from the last ${daysBack} days`
|
||||
);
|
||||
|
||||
let processedCount = 0;
|
||||
let candidateCount = 0;
|
||||
let triggeredCount = 0;
|
||||
|
||||
for (const issue of allIssues) {
|
||||
processedCount++;
|
||||
console.log(
|
||||
`[DEBUG] Processing issue #${issue.number} (${processedCount}/${allIssues.length}): ${issue.title}`
|
||||
);
|
||||
|
||||
console.log(`[DEBUG] Fetching comments for issue #${issue.number}...`);
|
||||
const comments = await githubRequest(
|
||||
`/repos/${owner}/${repo}/issues/${issue.number}/comments`,
|
||||
token
|
||||
);
|
||||
console.log(
|
||||
`[DEBUG] Issue #${issue.number} has ${comments.length} comments`
|
||||
);
|
||||
|
||||
// Look for existing duplicate detection comments (from the dedupe bot)
|
||||
const dupeDetectionComments = comments.filter(
|
||||
(comment) =>
|
||||
comment.body.includes('Found') &&
|
||||
comment.body.includes('possible duplicate') &&
|
||||
comment.user.type === 'Bot'
|
||||
);
|
||||
|
||||
console.log(
|
||||
`[DEBUG] Issue #${issue.number} has ${dupeDetectionComments.length} duplicate detection comments`
|
||||
);
|
||||
|
||||
// Skip if there's already a duplicate detection comment
|
||||
if (dupeDetectionComments.length > 0) {
|
||||
console.log(
|
||||
`[DEBUG] Issue #${issue.number} already has duplicate detection comment, skipping`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
candidateCount++;
|
||||
const issueUrl = `https://github.com/${owner}/${repo}/issues/${issue.number}`;
|
||||
|
||||
try {
|
||||
console.log(
|
||||
`[INFO] ${dryRun ? '[DRY RUN] ' : ''}Triggering dedupe workflow for issue #${issue.number}: ${issueUrl}`
|
||||
);
|
||||
await triggerDedupeWorkflow(owner, repo, issue.number, token, dryRun);
|
||||
|
||||
if (!dryRun) {
|
||||
console.log(
|
||||
`[SUCCESS] Successfully triggered dedupe workflow for issue #${issue.number}`
|
||||
);
|
||||
}
|
||||
triggeredCount++;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[ERROR] Failed to trigger workflow for issue #${issue.number}: ${error}`
|
||||
);
|
||||
}
|
||||
|
||||
// Add a delay between workflow triggers to avoid overwhelming the system
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[DEBUG] Script completed. Processed ${processedCount} issues, found ${candidateCount} candidates without duplicate comments, ${dryRun ? 'would trigger' : 'triggered'} ${triggeredCount} workflows`
|
||||
);
|
||||
}
|
||||
|
||||
backfillDuplicateComments().catch(console.error);
|
||||
54
.github/scripts/pre-release.mjs
vendored
54
.github/scripts/pre-release.mjs
vendored
@@ -1,54 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
import { readFileSync, existsSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import {
|
||||
findRootDir,
|
||||
runCommand,
|
||||
getPackageVersion,
|
||||
createAndPushTag
|
||||
} from './utils.mjs';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const rootDir = findRootDir(__dirname);
|
||||
const extensionPkgPath = join(rootDir, 'apps', 'extension', 'package.json');
|
||||
|
||||
console.log('🚀 Starting pre-release process...');
|
||||
|
||||
// Check if we're in RC mode
|
||||
const preJsonPath = join(rootDir, '.changeset', 'pre.json');
|
||||
if (!existsSync(preJsonPath)) {
|
||||
console.error('⚠️ Not in RC mode. Run "npx changeset pre enter rc" first.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
const preJson = JSON.parse(readFileSync(preJsonPath, 'utf8'));
|
||||
if (preJson.tag !== 'rc') {
|
||||
console.error(`⚠️ Not in RC mode. Current tag: ${preJson.tag}`);
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to read pre.json:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Get current extension version
|
||||
const extensionVersion = getPackageVersion(extensionPkgPath);
|
||||
console.log(`Extension version: ${extensionVersion}`);
|
||||
|
||||
// Run changeset publish for npm packages
|
||||
console.log('📦 Publishing npm packages...');
|
||||
runCommand('npx', ['changeset', 'publish']);
|
||||
|
||||
// Create tag for extension pre-release if it doesn't exist
|
||||
const extensionTag = `extension-rc@${extensionVersion}`;
|
||||
const tagCreated = createAndPushTag(extensionTag);
|
||||
|
||||
if (tagCreated) {
|
||||
console.log('This will trigger the extension-pre-release workflow...');
|
||||
}
|
||||
|
||||
console.log('✅ Pre-release process completed!');
|
||||
31
.github/workflows/auto-close-duplicates.yml
vendored
Normal file
31
.github/workflows/auto-close-duplicates.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
name: Auto-close duplicate issues
|
||||
# description: Auto-closes issues that are duplicates of existing issues
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 9 * * *" # Runs daily at 9 AM UTC
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
auto-close-duplicates:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write # Need write permission to close issues and add comments
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Auto-close duplicate issues
|
||||
run: node .github/scripts/auto-close-duplicates.mjs
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }}
|
||||
GITHUB_REPOSITORY_NAME: ${{ github.event.repository.name }}
|
||||
46
.github/workflows/backfill-duplicate-comments.yml
vendored
Normal file
46
.github/workflows/backfill-duplicate-comments.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
name: Backfill Duplicate Comments
|
||||
# description: Triggers duplicate detection for old issues that don't have duplicate comments
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
days_back:
|
||||
description: "How many days back to look for old issues"
|
||||
required: false
|
||||
default: "90"
|
||||
type: string
|
||||
dry_run:
|
||||
description: "Dry run mode (true to only log what would be done)"
|
||||
required: false
|
||||
default: "true"
|
||||
type: choice
|
||||
options:
|
||||
- "true"
|
||||
- "false"
|
||||
|
||||
jobs:
|
||||
backfill-duplicate-comments:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
contents: read
|
||||
issues: read
|
||||
actions: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Backfill duplicate comments
|
||||
run: node .github/scripts/backfill-duplicate-comments.mjs
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }}
|
||||
GITHUB_REPOSITORY_NAME: ${{ github.event.repository.name }}
|
||||
DAYS_BACK: ${{ inputs.days_back }}
|
||||
DRY_RUN: ${{ inputs.dry_run }}
|
||||
81
.github/workflows/claude-dedupe-issues.yml
vendored
Normal file
81
.github/workflows/claude-dedupe-issues.yml
vendored
Normal file
@@ -0,0 +1,81 @@
|
||||
name: Claude Issue Dedupe
|
||||
# description: Automatically dedupe GitHub issues using Claude Code
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
issue_number:
|
||||
description: "Issue number to process for duplicate detection"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
claude-dedupe-issues:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run Claude Code slash command
|
||||
uses: anthropics/claude-code-base-action@beta
|
||||
with:
|
||||
prompt: "/dedupe ${{ github.repository }}/issues/${{ github.event.issue.number || inputs.issue_number }}"
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
claude_env: |
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Log duplicate comment event to Statsig
|
||||
if: always()
|
||||
env:
|
||||
STATSIG_API_KEY: ${{ secrets.STATSIG_API_KEY }}
|
||||
run: |
|
||||
ISSUE_NUMBER=${{ github.event.issue.number || inputs.issue_number }}
|
||||
REPO=${{ github.repository }}
|
||||
|
||||
if [ -z "$STATSIG_API_KEY" ]; then
|
||||
echo "STATSIG_API_KEY not found, skipping Statsig logging"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Prepare the event payload
|
||||
EVENT_PAYLOAD=$(jq -n \
|
||||
--arg issue_number "$ISSUE_NUMBER" \
|
||||
--arg repo "$REPO" \
|
||||
--arg triggered_by "${{ github.event_name }}" \
|
||||
'{
|
||||
events: [{
|
||||
eventName: "github_duplicate_comment_added",
|
||||
value: 1,
|
||||
metadata: {
|
||||
repository: $repo,
|
||||
issue_number: ($issue_number | tonumber),
|
||||
triggered_by: $triggered_by,
|
||||
workflow_run_id: "${{ github.run_id }}"
|
||||
},
|
||||
time: (now | floor | tostring)
|
||||
}]
|
||||
}')
|
||||
|
||||
# Send to Statsig API
|
||||
echo "Logging duplicate comment event to Statsig for issue #${ISSUE_NUMBER}"
|
||||
|
||||
RESPONSE=$(curl -s -w "\n%{http_code}" -X POST https://events.statsigapi.net/v1/log_event \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "STATSIG-API-KEY: ${STATSIG_API_KEY}" \
|
||||
-d "$EVENT_PAYLOAD")
|
||||
|
||||
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
|
||||
BODY=$(echo "$RESPONSE" | head -n-1)
|
||||
|
||||
if [ "$HTTP_CODE" -eq 200 ] || [ "$HTTP_CODE" -eq 202 ]; then
|
||||
echo "Successfully logged duplicate comment event for issue #${ISSUE_NUMBER}"
|
||||
else
|
||||
echo "Failed to log duplicate comment event for issue #${ISSUE_NUMBER}. HTTP ${HTTP_CODE}: ${BODY}"
|
||||
fi
|
||||
156
.github/workflows/claude-docs-updater.yml
vendored
Normal file
156
.github/workflows/claude-docs-updater.yml
vendored
Normal file
@@ -0,0 +1,156 @@
|
||||
name: Claude Documentation Updater
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- next
|
||||
paths-ignore:
|
||||
- "apps/docs/**"
|
||||
- "*.md"
|
||||
- ".github/workflows/**"
|
||||
|
||||
jobs:
|
||||
update-docs:
|
||||
# Only run if changes were merged (not direct pushes from bots)
|
||||
if: github.actor != 'github-actions[bot]' && github.actor != 'dependabot[bot]'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
issues: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2 # Need previous commit for comparison
|
||||
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
run: |
|
||||
echo "Changed files in this push:"
|
||||
git diff --name-only HEAD^ HEAD | tee changed_files.txt
|
||||
|
||||
# Store changed files for Claude to analyze
|
||||
echo "changed_files<<EOF" >> $GITHUB_OUTPUT
|
||||
git diff --name-only HEAD^ HEAD >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
# Get the commit message and changes summary
|
||||
echo "commit_message<<EOF" >> $GITHUB_OUTPUT
|
||||
git log -1 --pretty=%B >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
# Get diff for documentation context
|
||||
echo "commit_diff<<EOF" >> $GITHUB_OUTPUT
|
||||
git diff HEAD^ HEAD --stat >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create docs update branch
|
||||
id: create-branch
|
||||
run: |
|
||||
BRANCH_NAME="docs/auto-update-$(date +%Y%m%d-%H%M%S)"
|
||||
git checkout -b $BRANCH_NAME
|
||||
echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Run Claude Code to Update Documentation
|
||||
uses: anthropics/claude-code-action@beta
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
timeout_minutes: "30"
|
||||
mode: "agent"
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
experimental_allowed_domains: |
|
||||
.anthropic.com
|
||||
.github.com
|
||||
api.github.com
|
||||
.githubusercontent.com
|
||||
registry.npmjs.org
|
||||
.task-master.dev
|
||||
base_branch: "next"
|
||||
direct_prompt: |
|
||||
You are a documentation specialist. Analyze the recent changes pushed to the 'next' branch and update the documentation accordingly.
|
||||
|
||||
Recent changes:
|
||||
- Commit: ${{ steps.changed-files.outputs.commit_message }}
|
||||
- Changed files:
|
||||
${{ steps.changed-files.outputs.changed_files }}
|
||||
|
||||
- Changes summary:
|
||||
${{ steps.changed-files.outputs.commit_diff }}
|
||||
|
||||
Your task:
|
||||
1. Analyze the changes to understand what functionality was added, modified, or removed
|
||||
2. Check if these changes require documentation updates in apps/docs/
|
||||
3. If documentation updates are needed:
|
||||
- Update relevant documentation files in apps/docs/
|
||||
- Ensure examples are updated if APIs changed
|
||||
- Update any configuration documentation if config options changed
|
||||
- Add new documentation pages if new features were added
|
||||
- Update the changelog or release notes if applicable
|
||||
4. If no documentation updates are needed, skip creating changes
|
||||
|
||||
Guidelines:
|
||||
- Focus only on user-facing changes that need documentation
|
||||
- Keep documentation clear, concise, and helpful
|
||||
- Include code examples where appropriate
|
||||
- Maintain consistent documentation style with existing docs
|
||||
- Don't document internal implementation details unless they affect users
|
||||
- Update navigation/menu files if new pages are added
|
||||
|
||||
Only make changes if the documentation truly needs updating based on the code changes.
|
||||
|
||||
- name: Check if changes were made
|
||||
id: check-changes
|
||||
run: |
|
||||
if git diff --quiet; then
|
||||
echo "has_changes=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "has_changes=true" >> $GITHUB_OUTPUT
|
||||
git add -A
|
||||
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git config --local user.name "github-actions[bot]"
|
||||
git commit -m "docs: auto-update documentation based on changes in next branch
|
||||
|
||||
This PR was automatically generated to update documentation based on recent changes.
|
||||
|
||||
Original commit: ${{ steps.changed-files.outputs.commit_message }}
|
||||
|
||||
Co-authored-by: Claude <claude-assistant@anthropic.com>"
|
||||
fi
|
||||
|
||||
- name: Push changes and create PR
|
||||
if: steps.check-changes.outputs.has_changes == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
git push origin ${{ steps.create-branch.outputs.branch_name }}
|
||||
|
||||
# Create PR using GitHub CLI
|
||||
gh pr create \
|
||||
--title "docs: update documentation for recent changes" \
|
||||
--body "## 📚 Documentation Update
|
||||
|
||||
This PR automatically updates documentation based on recent changes merged to the \`next\` branch.
|
||||
|
||||
### Original Changes
|
||||
**Commit:** ${{ github.sha }}
|
||||
**Message:** ${{ steps.changed-files.outputs.commit_message }}
|
||||
|
||||
### Changed Files in Original Commit
|
||||
\`\`\`
|
||||
${{ steps.changed-files.outputs.changed_files }}
|
||||
\`\`\`
|
||||
|
||||
### Documentation Updates
|
||||
This PR includes documentation updates to reflect the changes above. Please review to ensure:
|
||||
- [ ] Documentation accurately reflects the changes
|
||||
- [ ] Examples are correct and working
|
||||
- [ ] No important details are missing
|
||||
- [ ] Style is consistent with existing documentation
|
||||
|
||||
---
|
||||
*This PR was automatically generated by Claude Code GitHub Action*" \
|
||||
--base next \
|
||||
--head ${{ steps.create-branch.outputs.branch_name }} \
|
||||
--label "documentation" \
|
||||
--label "automated"
|
||||
107
.github/workflows/claude-issue-triage.yml
vendored
Normal file
107
.github/workflows/claude-issue-triage.yml
vendored
Normal file
@@ -0,0 +1,107 @@
|
||||
name: Claude Issue Triage
|
||||
# description: Automatically triage GitHub issues using Claude Code
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
triage-issue:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Create triage prompt
|
||||
run: |
|
||||
mkdir -p /tmp/claude-prompts
|
||||
cat > /tmp/claude-prompts/triage-prompt.txt << 'EOF'
|
||||
You're an issue triage assistant for GitHub issues. Your task is to analyze the issue and select appropriate labels from the provided list.
|
||||
|
||||
IMPORTANT: Don't post any comments or messages to the issue. Your only action should be to apply labels.
|
||||
|
||||
Issue Information:
|
||||
- REPO: ${{ github.repository }}
|
||||
- ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
|
||||
TASK OVERVIEW:
|
||||
|
||||
1. First, fetch the list of labels available in this repository by running: `gh label list`. Run exactly this command with nothing else.
|
||||
|
||||
2. Next, use the GitHub tools to get context about the issue:
|
||||
- You have access to these tools:
|
||||
- mcp__github__get_issue: Use this to retrieve the current issue's details including title, description, and existing labels
|
||||
- mcp__github__get_issue_comments: Use this to read any discussion or additional context provided in the comments
|
||||
- mcp__github__update_issue: Use this to apply labels to the issue (do not use this for commenting)
|
||||
- mcp__github__search_issues: Use this to find similar issues that might provide context for proper categorization and to identify potential duplicate issues
|
||||
- mcp__github__list_issues: Use this to understand patterns in how other issues are labeled
|
||||
- Start by using mcp__github__get_issue to get the issue details
|
||||
|
||||
3. Analyze the issue content, considering:
|
||||
- The issue title and description
|
||||
- The type of issue (bug report, feature request, question, etc.)
|
||||
- Technical areas mentioned
|
||||
- Severity or priority indicators
|
||||
- User impact
|
||||
- Components affected
|
||||
|
||||
4. Select appropriate labels from the available labels list provided above:
|
||||
- Choose labels that accurately reflect the issue's nature
|
||||
- Be specific but comprehensive
|
||||
- Select priority labels if you can determine urgency (high-priority, med-priority, or low-priority)
|
||||
- Consider platform labels (android, ios) if applicable
|
||||
- If you find similar issues using mcp__github__search_issues, consider using a "duplicate" label if appropriate. Only do so if the issue is a duplicate of another OPEN issue.
|
||||
|
||||
5. Apply the selected labels:
|
||||
- Use mcp__github__update_issue to apply your selected labels
|
||||
- DO NOT post any comments explaining your decision
|
||||
- DO NOT communicate directly with users
|
||||
- If no labels are clearly applicable, do not apply any labels
|
||||
|
||||
IMPORTANT GUIDELINES:
|
||||
- Be thorough in your analysis
|
||||
- Only select labels from the provided list above
|
||||
- DO NOT post any comments to the issue
|
||||
- Your ONLY action should be to apply labels using mcp__github__update_issue
|
||||
- It's okay to not add any labels if none are clearly applicable
|
||||
EOF
|
||||
|
||||
- name: Setup GitHub MCP Server
|
||||
run: |
|
||||
mkdir -p /tmp/mcp-config
|
||||
cat > /tmp/mcp-config/mcp-servers.json << 'EOF'
|
||||
{
|
||||
"mcpServers": {
|
||||
"github": {
|
||||
"command": "docker",
|
||||
"args": [
|
||||
"run",
|
||||
"-i",
|
||||
"--rm",
|
||||
"-e",
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN",
|
||||
"ghcr.io/github/github-mcp-server:sha-7aced2b"
|
||||
],
|
||||
"env": {
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.GITHUB_TOKEN }}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
- name: Run Claude Code for Issue Triage
|
||||
uses: anthropics/claude-code-base-action@beta
|
||||
with:
|
||||
prompt_file: /tmp/claude-prompts/triage-prompt.txt
|
||||
allowed_tools: "Bash(gh label list),mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__update_issue,mcp__github__search_issues,mcp__github__list_issues"
|
||||
timeout_minutes: "5"
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
mcp_config: /tmp/mcp-config/mcp-servers.json
|
||||
claude_env: |
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
36
.github/workflows/claude.yml
vendored
Normal file
36
.github/workflows/claude.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: Claude Code
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
issues:
|
||||
types: [opened, assigned]
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
jobs:
|
||||
claude:
|
||||
if: |
|
||||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
|
||||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: read
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude Code
|
||||
id: claude
|
||||
uses: anthropics/claude-code-action@beta
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
110
.github/workflows/extension-pre-release.yml
vendored
110
.github/workflows/extension-pre-release.yml
vendored
@@ -1,110 +0,0 @@
|
||||
name: Extension Pre-Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "extension-rc@*"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
concurrency: extension-pre-release-${{ github.ref }}
|
||||
|
||||
jobs:
|
||||
publish-extension-rc:
|
||||
runs-on: ubuntu-latest
|
||||
environment: extension-release
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
node_modules
|
||||
*/*/node_modules
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
|
||||
- name: Install Extension Dependencies
|
||||
working-directory: apps/extension
|
||||
run: npm ci
|
||||
timeout-minutes: 5
|
||||
|
||||
- name: Type Check Extension
|
||||
working-directory: apps/extension
|
||||
run: npm run check-types
|
||||
env:
|
||||
FORCE_COLOR: 1
|
||||
|
||||
- name: Build Extension
|
||||
working-directory: apps/extension
|
||||
run: npm run build
|
||||
env:
|
||||
FORCE_COLOR: 1
|
||||
|
||||
- name: Package Extension
|
||||
working-directory: apps/extension
|
||||
run: npm run package
|
||||
env:
|
||||
FORCE_COLOR: 1
|
||||
|
||||
- name: Create VSIX Package (Pre-Release)
|
||||
working-directory: apps/extension/vsix-build
|
||||
run: npx vsce package --no-dependencies --pre-release
|
||||
env:
|
||||
FORCE_COLOR: 1
|
||||
|
||||
- name: Get VSIX filename
|
||||
id: vsix-info
|
||||
working-directory: apps/extension/vsix-build
|
||||
run: |
|
||||
VSIX_FILE=$(find . -maxdepth 1 -name "*.vsix" -type f | head -n1 | xargs basename)
|
||||
if [ -z "$VSIX_FILE" ]; then
|
||||
echo "Error: No VSIX file found"
|
||||
exit 1
|
||||
fi
|
||||
echo "vsix-filename=$VSIX_FILE" >> "$GITHUB_OUTPUT"
|
||||
echo "Found VSIX: $VSIX_FILE"
|
||||
|
||||
- name: Publish to VS Code Marketplace (Pre-Release)
|
||||
working-directory: apps/extension/vsix-build
|
||||
run: npx vsce publish --packagePath "${{ steps.vsix-info.outputs.vsix-filename }}" --pre-release
|
||||
env:
|
||||
VSCE_PAT: ${{ secrets.VSCE_PAT }}
|
||||
FORCE_COLOR: 1
|
||||
|
||||
- name: Install Open VSX CLI
|
||||
run: npm install -g ovsx
|
||||
|
||||
- name: Publish to Open VSX Registry (Pre-Release)
|
||||
working-directory: apps/extension/vsix-build
|
||||
run: ovsx publish "${{ steps.vsix-info.outputs.vsix-filename }}" --pre-release
|
||||
env:
|
||||
OVSX_PAT: ${{ secrets.OVSX_PAT }}
|
||||
FORCE_COLOR: 1
|
||||
|
||||
- name: Upload Build Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: extension-pre-release-${{ github.ref_name }}
|
||||
path: |
|
||||
apps/extension/vsix-build/*.vsix
|
||||
apps/extension/dist/
|
||||
retention-days: 30
|
||||
|
||||
notify-success:
|
||||
needs: publish-extension-rc
|
||||
if: success()
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Success Notification
|
||||
run: |
|
||||
echo "🚀 Extension ${{ github.ref_name }} successfully published as pre-release!"
|
||||
echo "📦 Available on VS Code Marketplace (Pre-Release)"
|
||||
echo "🌍 Available on Open VSX Registry (Pre-Release)"
|
||||
176
.github/workflows/log-issue-events.yml
vendored
Normal file
176
.github/workflows/log-issue-events.yml
vendored
Normal file
@@ -0,0 +1,176 @@
|
||||
name: Log GitHub Issue Events
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, closed]
|
||||
|
||||
jobs:
|
||||
log-issue-created:
|
||||
if: github.event.action == 'opened'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
contents: read
|
||||
issues: read
|
||||
|
||||
steps:
|
||||
- name: Log issue creation to Statsig
|
||||
env:
|
||||
STATSIG_API_KEY: ${{ secrets.STATSIG_API_KEY }}
|
||||
run: |
|
||||
ISSUE_NUMBER=${{ github.event.issue.number }}
|
||||
REPO=${{ github.repository }}
|
||||
ISSUE_TITLE=$(echo '${{ github.event.issue.title }}' | sed "s/'/'\\\\''/g")
|
||||
AUTHOR="${{ github.event.issue.user.login }}"
|
||||
CREATED_AT="${{ github.event.issue.created_at }}"
|
||||
|
||||
if [ -z "$STATSIG_API_KEY" ]; then
|
||||
echo "STATSIG_API_KEY not found, skipping Statsig logging"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Prepare the event payload
|
||||
EVENT_PAYLOAD=$(jq -n \
|
||||
--arg issue_number "$ISSUE_NUMBER" \
|
||||
--arg repo "$REPO" \
|
||||
--arg title "$ISSUE_TITLE" \
|
||||
--arg author "$AUTHOR" \
|
||||
--arg created_at "$CREATED_AT" \
|
||||
'{
|
||||
events: [{
|
||||
eventName: "github_issue_created",
|
||||
value: 1,
|
||||
metadata: {
|
||||
repository: $repo,
|
||||
issue_number: ($issue_number | tonumber),
|
||||
issue_title: $title,
|
||||
issue_author: $author,
|
||||
created_at: $created_at
|
||||
},
|
||||
time: (now | floor | tostring)
|
||||
}]
|
||||
}')
|
||||
|
||||
# Send to Statsig API
|
||||
echo "Logging issue creation to Statsig for issue #${ISSUE_NUMBER}"
|
||||
|
||||
RESPONSE=$(curl -s -w "\n%{http_code}" -X POST https://events.statsigapi.net/v1/log_event \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "STATSIG-API-KEY: ${STATSIG_API_KEY}" \
|
||||
-d "$EVENT_PAYLOAD")
|
||||
|
||||
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
|
||||
BODY=$(echo "$RESPONSE" | head -n-1)
|
||||
|
||||
if [ "$HTTP_CODE" -eq 200 ] || [ "$HTTP_CODE" -eq 202 ]; then
|
||||
echo "Successfully logged issue creation for issue #${ISSUE_NUMBER}"
|
||||
else
|
||||
echo "Failed to log issue creation for issue #${ISSUE_NUMBER}. HTTP ${HTTP_CODE}: ${BODY}"
|
||||
fi
|
||||
|
||||
log-issue-closed:
|
||||
if: github.event.action == 'closed'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
contents: read
|
||||
issues: read
|
||||
|
||||
steps:
|
||||
- name: Log issue closure to Statsig
|
||||
env:
|
||||
STATSIG_API_KEY: ${{ secrets.STATSIG_API_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
ISSUE_NUMBER=${{ github.event.issue.number }}
|
||||
REPO=${{ github.repository }}
|
||||
ISSUE_TITLE=$(echo '${{ github.event.issue.title }}' | sed "s/'/'\\\\''/g")
|
||||
CLOSED_BY="${{ github.event.issue.closed_by.login }}"
|
||||
CLOSED_AT="${{ github.event.issue.closed_at }}"
|
||||
STATE_REASON="${{ github.event.issue.state_reason }}"
|
||||
|
||||
if [ -z "$STATSIG_API_KEY" ]; then
|
||||
echo "STATSIG_API_KEY not found, skipping Statsig logging"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Get additional issue data via GitHub API
|
||||
echo "Fetching additional issue data for #${ISSUE_NUMBER}"
|
||||
ISSUE_DATA=$(curl -s -H "Authorization: token ${GITHUB_TOKEN}" \
|
||||
-H "Accept: application/vnd.github.v3+json" \
|
||||
"https://api.github.com/repos/${REPO}/issues/${ISSUE_NUMBER}")
|
||||
|
||||
COMMENTS_COUNT=$(echo "$ISSUE_DATA" | jq -r '.comments')
|
||||
|
||||
# Get reactions data
|
||||
REACTIONS_DATA=$(curl -s -H "Authorization: token ${GITHUB_TOKEN}" \
|
||||
-H "Accept: application/vnd.github.v3+json" \
|
||||
"https://api.github.com/repos/${REPO}/issues/${ISSUE_NUMBER}/reactions")
|
||||
|
||||
REACTIONS_COUNT=$(echo "$REACTIONS_DATA" | jq '. | length')
|
||||
|
||||
# Check if issue was closed automatically (by checking if closed_by is a bot)
|
||||
CLOSED_AUTOMATICALLY="false"
|
||||
if [[ "$CLOSED_BY" == *"[bot]"* ]]; then
|
||||
CLOSED_AUTOMATICALLY="true"
|
||||
fi
|
||||
|
||||
# Check if closed as duplicate by state_reason
|
||||
CLOSED_AS_DUPLICATE="false"
|
||||
if [ "$STATE_REASON" = "duplicate" ]; then
|
||||
CLOSED_AS_DUPLICATE="true"
|
||||
fi
|
||||
|
||||
# Prepare the event payload
|
||||
EVENT_PAYLOAD=$(jq -n \
|
||||
--arg issue_number "$ISSUE_NUMBER" \
|
||||
--arg repo "$REPO" \
|
||||
--arg title "$ISSUE_TITLE" \
|
||||
--arg closed_by "$CLOSED_BY" \
|
||||
--arg closed_at "$CLOSED_AT" \
|
||||
--arg state_reason "$STATE_REASON" \
|
||||
--arg comments_count "$COMMENTS_COUNT" \
|
||||
--arg reactions_count "$REACTIONS_COUNT" \
|
||||
--arg closed_automatically "$CLOSED_AUTOMATICALLY" \
|
||||
--arg closed_as_duplicate "$CLOSED_AS_DUPLICATE" \
|
||||
'{
|
||||
events: [{
|
||||
eventName: "github_issue_closed",
|
||||
value: 1,
|
||||
metadata: {
|
||||
repository: $repo,
|
||||
issue_number: ($issue_number | tonumber),
|
||||
issue_title: $title,
|
||||
closed_by: $closed_by,
|
||||
closed_at: $closed_at,
|
||||
state_reason: $state_reason,
|
||||
comments_count: ($comments_count | tonumber),
|
||||
reactions_count: ($reactions_count | tonumber),
|
||||
closed_automatically: ($closed_automatically | test("true")),
|
||||
closed_as_duplicate: ($closed_as_duplicate | test("true"))
|
||||
},
|
||||
time: (now | floor | tostring)
|
||||
}]
|
||||
}')
|
||||
|
||||
# Send to Statsig API
|
||||
echo "Logging issue closure to Statsig for issue #${ISSUE_NUMBER}"
|
||||
|
||||
RESPONSE=$(curl -s -w "\n%{http_code}" -X POST https://events.statsigapi.net/v1/log_event \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "STATSIG-API-KEY: ${STATSIG_API_KEY}" \
|
||||
-d "$EVENT_PAYLOAD")
|
||||
|
||||
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
|
||||
BODY=$(echo "$RESPONSE" | head -n-1)
|
||||
|
||||
if [ "$HTTP_CODE" -eq 200 ] || [ "$HTTP_CODE" -eq 202 ]; then
|
||||
echo "Successfully logged issue closure for issue #${ISSUE_NUMBER}"
|
||||
echo "Closed by: $CLOSED_BY"
|
||||
echo "Comments: $COMMENTS_COUNT"
|
||||
echo "Reactions: $REACTIONS_COUNT"
|
||||
echo "Closed automatically: $CLOSED_AUTOMATICALLY"
|
||||
echo "Closed as duplicate: $CLOSED_AS_DUPLICATE"
|
||||
else
|
||||
echo "Failed to log issue closure for issue #${ISSUE_NUMBER}. HTTP ${HTTP_CODE}: ${BODY}"
|
||||
fi
|
||||
4
.github/workflows/pre-release.yml
vendored
4
.github/workflows/pre-release.yml
vendored
@@ -68,12 +68,10 @@ jobs:
|
||||
- name: Create Release Candidate Pull Request or Publish Release Candidate to npm
|
||||
uses: changesets/action@v1
|
||||
with:
|
||||
publish: node ./.github/scripts/pre-release.mjs
|
||||
publish: npx changeset publish
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
VSCE_PAT: ${{ secrets.VSCE_PAT }}
|
||||
OVSX_PAT: ${{ secrets.OVSX_PAT }}
|
||||
|
||||
- name: Commit & Push changes
|
||||
uses: actions-js/push@master
|
||||
|
||||
96
.github/workflows/weekly-metrics-discord.yml
vendored
Normal file
96
.github/workflows/weekly-metrics-discord.yml
vendored
Normal file
@@ -0,0 +1,96 @@
|
||||
name: Weekly Metrics to Discord
|
||||
# description: Sends weekly metrics summary to Discord channel
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 9 * * 1" # Every Monday at 9 AM
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
weekly-metrics:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_METRICS_WEBHOOK }}
|
||||
steps:
|
||||
- name: Get dates for last week
|
||||
run: |
|
||||
# Last 7 days
|
||||
first_day=$(date -d "7 days ago" +%Y-%m-%d)
|
||||
last_day=$(date +%Y-%m-%d)
|
||||
|
||||
echo "first_day=$first_day" >> $GITHUB_ENV
|
||||
echo "last_day=$last_day" >> $GITHUB_ENV
|
||||
echo "week_of=$(date -d '7 days ago' +'Week of %B %d, %Y')" >> $GITHUB_ENV
|
||||
|
||||
- name: Generate issue metrics
|
||||
uses: github/issue-metrics@v3
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SEARCH_QUERY: "repo:${{ github.repository }} is:issue created:${{ env.first_day }}..${{ env.last_day }}"
|
||||
HIDE_TIME_TO_ANSWER: true
|
||||
HIDE_LABEL_METRICS: false
|
||||
|
||||
- name: Generate PR metrics
|
||||
uses: github/issue-metrics@v3
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SEARCH_QUERY: "repo:${{ github.repository }} is:pr created:${{ env.first_day }}..${{ env.last_day }}"
|
||||
OUTPUT_FILE: pr_metrics.md
|
||||
|
||||
- name: Parse metrics
|
||||
id: metrics
|
||||
run: |
|
||||
# Parse the metrics from the generated markdown files
|
||||
if [ -f "issue_metrics.md" ]; then
|
||||
# Extract key metrics using grep/awk
|
||||
AVG_TIME_TO_FIRST_RESPONSE=$(grep -A 1 "Average time to first response" issue_metrics.md | tail -1 | xargs || echo "N/A")
|
||||
AVG_TIME_TO_CLOSE=$(grep -A 1 "Average time to close" issue_metrics.md | tail -1 | xargs || echo "N/A")
|
||||
NUM_ISSUES_CREATED=$(grep -oP '\d+(?= issues created)' issue_metrics.md || echo "0")
|
||||
NUM_ISSUES_CLOSED=$(grep -oP '\d+(?= issues closed)' issue_metrics.md || echo "0")
|
||||
fi
|
||||
|
||||
if [ -f "pr_metrics.md" ]; then
|
||||
PR_AVG_TIME_TO_MERGE=$(grep -A 1 "Average time to close" pr_metrics.md | tail -1 | xargs || echo "N/A")
|
||||
NUM_PRS_CREATED=$(grep -oP '\d+(?= pull requests created)' pr_metrics.md || echo "0")
|
||||
NUM_PRS_MERGED=$(grep -oP '\d+(?= pull requests closed)' pr_metrics.md || echo "0")
|
||||
fi
|
||||
|
||||
# Set outputs for Discord action
|
||||
echo "issues_created=${NUM_ISSUES_CREATED:-0}" >> $GITHUB_OUTPUT
|
||||
echo "issues_closed=${NUM_ISSUES_CLOSED:-0}" >> $GITHUB_OUTPUT
|
||||
echo "prs_created=${NUM_PRS_CREATED:-0}" >> $GITHUB_OUTPUT
|
||||
echo "prs_merged=${NUM_PRS_MERGED:-0}" >> $GITHUB_OUTPUT
|
||||
echo "avg_first_response=${AVG_TIME_TO_FIRST_RESPONSE:-N/A}" >> $GITHUB_OUTPUT
|
||||
echo "avg_time_to_close=${AVG_TIME_TO_CLOSE:-N/A}" >> $GITHUB_OUTPUT
|
||||
echo "pr_avg_merge_time=${PR_AVG_TIME_TO_MERGE:-N/A}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Send to Discord
|
||||
uses: sarisia/actions-status-discord@v1
|
||||
if: env.DISCORD_WEBHOOK != ''
|
||||
with:
|
||||
webhook: ${{ env.DISCORD_WEBHOOK }}
|
||||
status: Success
|
||||
title: "📊 Weekly Metrics Report"
|
||||
description: |
|
||||
**${{ env.week_of }}**
|
||||
|
||||
**🎯 Issues**
|
||||
• Created: ${{ steps.metrics.outputs.issues_created }}
|
||||
• Closed: ${{ steps.metrics.outputs.issues_closed }}
|
||||
|
||||
**🔀 Pull Requests**
|
||||
• Created: ${{ steps.metrics.outputs.prs_created }}
|
||||
• Merged: ${{ steps.metrics.outputs.prs_merged }}
|
||||
|
||||
**⏱️ Response Times**
|
||||
• First Response: ${{ steps.metrics.outputs.avg_first_response }}
|
||||
• Time to Close: ${{ steps.metrics.outputs.avg_time_to_close }}
|
||||
• PR Merge Time: ${{ steps.metrics.outputs.pr_avg_merge_time }}
|
||||
color: 0x58AFFF
|
||||
username: Task Master Metrics Bot
|
||||
avatar_url: https://raw.githubusercontent.com/eyaltoledano/claude-task-master/main/images/logo.png
|
||||
8
.taskmaster/docs/test-prd.txt
Normal file
8
.taskmaster/docs/test-prd.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
Simple Todo App PRD
|
||||
|
||||
Create a basic todo list application with the following features:
|
||||
1. Add new todos
|
||||
2. Mark todos as complete
|
||||
3. Delete todos
|
||||
|
||||
That's it. Keep it simple.
|
||||
151
CHANGELOG.md
151
CHANGELOG.md
@@ -1,5 +1,156 @@
|
||||
# task-master-ai
|
||||
|
||||
## 0.26.0-rc.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#1163](https://github.com/eyaltoledano/claude-task-master/pull/1163) [`37af0f1`](https://github.com/eyaltoledano/claude-task-master/commit/37af0f191227a68d119b7f89a377bf932ee3ac66) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Enhanced Gemini CLI provider with codebase-aware task generation
|
||||
|
||||
Added automatic codebase analysis for Gemini CLI provider in parse-prd, and analyze-complexity, add-task, udpate-task, update, update-subtask commands
|
||||
When using Gemini CLI as the AI provider, Task Master now instructs the AI to analyze the project structure, existing implementations, and patterns before generating tasks or subtasks
|
||||
Tasks and subtasks generated by Claude Code are now informed by actual codebase analysis, resulting in more accurate and contextual outputs
|
||||
|
||||
- [#1135](https://github.com/eyaltoledano/claude-task-master/pull/1135) [`8783708`](https://github.com/eyaltoledano/claude-task-master/commit/8783708e5e3389890a78fcf685d3da0580e73b3f) Thanks [@mm-parthy](https://github.com/mm-parthy)! - feat(move): improve cross-tag move UX and safety
|
||||
- CLI: print "Next Steps" tips after cross-tag moves that used --ignore-dependencies (validate/fix guidance)
|
||||
- CLI: show dedicated help block on ID collisions (destination tag already has the ID)
|
||||
- Core: add structured suggestions to TASK_ALREADY_EXISTS errors
|
||||
- MCP: map ID collision errors to TASK_ALREADY_EXISTS and include suggestions
|
||||
- Tests: cover MCP options, error suggestions, CLI tips printing, and integration error payload suggestions
|
||||
|
||||
***
|
||||
|
||||
- [#1162](https://github.com/eyaltoledano/claude-task-master/pull/1162) [`4dad2fd`](https://github.com/eyaltoledano/claude-task-master/commit/4dad2fd613ceac56a65ae9d3c1c03092b8860ac9) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Enhanced Claude Code and Google CLI integration with automatic codebase analysis for task operations
|
||||
|
||||
When using Claude Code as the AI provider, task management commands now automatically analyze your codebase before generating or updating tasks. This provides more accurate, context-aware implementation details that align with your project's existing architecture and patterns.
|
||||
|
||||
Commands contextualised:
|
||||
- add-task
|
||||
- update-subtask
|
||||
- update-task
|
||||
- update
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#1135](https://github.com/eyaltoledano/claude-task-master/pull/1135) [`8783708`](https://github.com/eyaltoledano/claude-task-master/commit/8783708e5e3389890a78fcf685d3da0580e73b3f) Thanks [@mm-parthy](https://github.com/mm-parthy)! - docs(move): clarify cross-tag move docs; deprecate "force"; add explicit --with-dependencies/--ignore-dependencies examples
|
||||
|
||||
## 0.25.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#1152](https://github.com/eyaltoledano/claude-task-master/pull/1152) [`8933557`](https://github.com/eyaltoledano/claude-task-master/commit/89335578ffffc65504b2055c0c85aa7521e5e79b) Thanks [@ben-vargas](https://github.com/ben-vargas)! - fix(claude-code): prevent crash/hang when the optional `@anthropic-ai/claude-code` SDK is missing by guarding `AbortError instanceof` checks and adding explicit SDK presence checks in `doGenerate`/`doStream`. Also bump the optional dependency to `^1.0.88` for improved export consistency.
|
||||
|
||||
Related to JSON truncation handling in #920; this change addresses a separate error-path crash reported in #1142.
|
||||
|
||||
- [#1151](https://github.com/eyaltoledano/claude-task-master/pull/1151) [`db720a9`](https://github.com/eyaltoledano/claude-task-master/commit/db720a954d390bb44838cd021b8813dde8f3d8de) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Temporarily disable streaming for improved model compatibility - will be re-enabled in upcoming release
|
||||
|
||||
## 0.25.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#1088](https://github.com/eyaltoledano/claude-task-master/pull/1088) [`04e11b5`](https://github.com/eyaltoledano/claude-task-master/commit/04e11b5e828597c0ba5b82ca7d5fb6f933e4f1e8) Thanks [@mm-parthy](https://github.com/mm-parthy)! - Add cross-tag task movement functionality for organizing tasks across different contexts.
|
||||
|
||||
This feature enables moving tasks between different tags (contexts) in your project, making it easier to organize work across different branches, environments, or project phases.
|
||||
|
||||
## CLI Usage Examples
|
||||
|
||||
Move a single task from one tag to another:
|
||||
|
||||
```bash
|
||||
# Move task 5 from backlog tag to in-progress tag
|
||||
task-master move --from=5 --from-tag=backlog --to-tag=feature-1
|
||||
|
||||
# Move task with its dependencies
|
||||
task-master move --from=5 --from-tag=backlog --to-tag=feature-2 --with-dependencies
|
||||
|
||||
# Move task without checking dependencies
|
||||
task-master move --from=5 --from-tag=backlog --to-tag=bug-3 --ignore-dependencies
|
||||
```
|
||||
|
||||
Move multiple tasks at once:
|
||||
|
||||
```bash
|
||||
# Move multiple tasks between tags
|
||||
task-master move --from=5,6,7 --from-tag=backlog --to-tag=bug-4 --with-dependencies
|
||||
```
|
||||
|
||||
- [#1040](https://github.com/eyaltoledano/claude-task-master/pull/1040) [`fc47714`](https://github.com/eyaltoledano/claude-task-master/commit/fc477143400fd11d953727bf1b4277af5ad308d1) Thanks [@DomVidja](https://github.com/DomVidja)! - "Add Kilo Code profile integration with custom modes and MCP configuration"
|
||||
|
||||
- [#1054](https://github.com/eyaltoledano/claude-task-master/pull/1054) [`782728f`](https://github.com/eyaltoledano/claude-task-master/commit/782728ff95aa2e3b766d48273b57f6c6753e8573) Thanks [@martincik](https://github.com/martincik)! - Add compact mode --compact / -c flag to the `tm list` CLI command
|
||||
- outputs tasks in a minimal, git-style one-line format. This reduces verbose output from ~30+ lines of dashboards and tables to just 1 line per task, making it much easier to quickly scan available tasks.
|
||||
- Git-style format: ID STATUS TITLE (PRIORITY) → DEPS
|
||||
- Color-coded status, priority, and dependencies
|
||||
- Smart title truncation and dependency abbreviation
|
||||
- Subtask support with indentation
|
||||
- Full backward compatibility with existing list options
|
||||
|
||||
- [#1048](https://github.com/eyaltoledano/claude-task-master/pull/1048) [`e3ed4d7`](https://github.com/eyaltoledano/claude-task-master/commit/e3ed4d7c14b56894d7da675eb2b757423bea8f9d) Thanks [@joedanz](https://github.com/joedanz)! - Add CLI & MCP progress tracking for parse-prd command.
|
||||
|
||||
- [#1124](https://github.com/eyaltoledano/claude-task-master/pull/1124) [`95640dc`](https://github.com/eyaltoledano/claude-task-master/commit/95640dcde87ce7879858c0a951399fb49f3b6397) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Add support for ollama `gpt-oss:20b` and `gpt-oss:120b`
|
||||
|
||||
- [#1123](https://github.com/eyaltoledano/claude-task-master/pull/1123) [`311b243`](https://github.com/eyaltoledano/claude-task-master/commit/311b2433e23c771c8d3a4d3f5ac577302b8321e5) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Remove `clear` Taskmaster claude code commands since they were too close to the claude-code clear command
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#1131](https://github.com/eyaltoledano/claude-task-master/pull/1131) [`3dee60d`](https://github.com/eyaltoledano/claude-task-master/commit/3dee60dc3d566e3cff650accb30f994b8bb3a15e) Thanks [@joedanz](https://github.com/joedanz)! - Update Cursor one-click install link to new URL format
|
||||
|
||||
- [#1088](https://github.com/eyaltoledano/claude-task-master/pull/1088) [`04e11b5`](https://github.com/eyaltoledano/claude-task-master/commit/04e11b5e828597c0ba5b82ca7d5fb6f933e4f1e8) Thanks [@mm-parthy](https://github.com/mm-parthy)! - Fix `add-tag --from-branch` command error where `projectRoot` was not properly referenced
|
||||
|
||||
The command was failing with "projectRoot is not defined" error because the code was directly referencing `projectRoot` instead of `context.projectRoot` in the git repository checks. This fix corrects the variable references to use the proper context object.
|
||||
|
||||
## 0.25.0-rc.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#1088](https://github.com/eyaltoledano/claude-task-master/pull/1088) [`04e11b5`](https://github.com/eyaltoledano/claude-task-master/commit/04e11b5e828597c0ba5b82ca7d5fb6f933e4f1e8) Thanks [@mm-parthy](https://github.com/mm-parthy)! - Add cross-tag task movement functionality for organizing tasks across different contexts.
|
||||
|
||||
This feature enables moving tasks between different tags (contexts) in your project, making it easier to organize work across different branches, environments, or project phases.
|
||||
|
||||
## CLI Usage Examples
|
||||
|
||||
Move a single task from one tag to another:
|
||||
|
||||
```bash
|
||||
# Move task 5 from backlog tag to in-progress tag
|
||||
task-master move --from=5 --from-tag=backlog --to-tag=feature-1
|
||||
|
||||
# Move task with its dependencies
|
||||
task-master move --from=5 --from-tag=backlog --to-tag=feature-2 --with-dependencies
|
||||
|
||||
# Move task without checking dependencies
|
||||
task-master move --from=5 --from-tag=backlog --to-tag=bug-3 --ignore-dependencies
|
||||
```
|
||||
|
||||
Move multiple tasks at once:
|
||||
|
||||
```bash
|
||||
# Move multiple tasks between tags
|
||||
task-master move --from=5,6,7 --from-tag=backlog --to-tag=bug-4 --with-dependencies
|
||||
```
|
||||
|
||||
- [#1040](https://github.com/eyaltoledano/claude-task-master/pull/1040) [`fc47714`](https://github.com/eyaltoledano/claude-task-master/commit/fc477143400fd11d953727bf1b4277af5ad308d1) Thanks [@DomVidja](https://github.com/DomVidja)! - "Add Kilo Code profile integration with custom modes and MCP configuration"
|
||||
|
||||
- [#1054](https://github.com/eyaltoledano/claude-task-master/pull/1054) [`782728f`](https://github.com/eyaltoledano/claude-task-master/commit/782728ff95aa2e3b766d48273b57f6c6753e8573) Thanks [@martincik](https://github.com/martincik)! - Add compact mode --compact / -c flag to the `tm list` CLI command
|
||||
- outputs tasks in a minimal, git-style one-line format. This reduces verbose output from ~30+ lines of dashboards and tables to just 1 line per task, making it much easier to quickly scan available tasks.
|
||||
- Git-style format: ID STATUS TITLE (PRIORITY) → DEPS
|
||||
- Color-coded status, priority, and dependencies
|
||||
- Smart title truncation and dependency abbreviation
|
||||
- Subtask support with indentation
|
||||
- Full backward compatibility with existing list options
|
||||
|
||||
- [#1048](https://github.com/eyaltoledano/claude-task-master/pull/1048) [`e3ed4d7`](https://github.com/eyaltoledano/claude-task-master/commit/e3ed4d7c14b56894d7da675eb2b757423bea8f9d) Thanks [@joedanz](https://github.com/joedanz)! - Add CLI & MCP progress tracking for parse-prd command.
|
||||
|
||||
- [#1124](https://github.com/eyaltoledano/claude-task-master/pull/1124) [`95640dc`](https://github.com/eyaltoledano/claude-task-master/commit/95640dcde87ce7879858c0a951399fb49f3b6397) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Add support for ollama `gpt-oss:20b` and `gpt-oss:120b`
|
||||
|
||||
- [#1123](https://github.com/eyaltoledano/claude-task-master/pull/1123) [`311b243`](https://github.com/eyaltoledano/claude-task-master/commit/311b2433e23c771c8d3a4d3f5ac577302b8321e5) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Remove `clear` Taskmaster claude code commands since they were too close to the claude-code clear command
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#1131](https://github.com/eyaltoledano/claude-task-master/pull/1131) [`3dee60d`](https://github.com/eyaltoledano/claude-task-master/commit/3dee60dc3d566e3cff650accb30f994b8bb3a15e) Thanks [@joedanz](https://github.com/joedanz)! - Update Cursor one-click install link to new URL format
|
||||
|
||||
- [#1088](https://github.com/eyaltoledano/claude-task-master/pull/1088) [`04e11b5`](https://github.com/eyaltoledano/claude-task-master/commit/04e11b5e828597c0ba5b82ca7d5fb6f933e4f1e8) Thanks [@mm-parthy](https://github.com/mm-parthy)! - Fix `add-tag --from-branch` command error where `projectRoot` was not properly referenced
|
||||
|
||||
The command was failing with "projectRoot is not defined" error because the code was directly referencing `projectRoot` instead of `context.projectRoot` in the git repository checks. This fix corrects the variable references to use the proper context object.
|
||||
|
||||
## 0.24.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -56,7 +56,7 @@ The following documentation is also available in the `docs` directory:
|
||||
|
||||
#### Quick Install for Cursor 1.0+ (One-Click)
|
||||
|
||||
[](https://cursor.com/install-mcp?name=task-master-ai&config=eyJjb21tYW5kIjoibnB4IC15IC0tcGFja2FnZT10YXNrLW1hc3Rlci1haSB0YXNrLW1hc3Rlci1haSIsImVudiI6eyJBTlRIUk9QSUNfQVBJX0tFWSI6IllPVVJfQU5USFJPUElDX0FQSV9LRVlfSEVSRSIsIlBFUlBMRVhJVFlfQVBJX0tFWSI6IllPVVJfUEVSUExFWElUWV9BUElfS0VZX0hFUkUiLCJPUEVOQUlfQVBJX0tFWSI6IllPVVJfT1BFTkFJX0tFWV9IRVJFIiwiR09PR0xFX0FQSV9LRVkiOiJZT1VSX0dPT0dMRV9LRVlfSEVSRSIsIk1JU1RSQUxfQVBJX0tFWSI6IllPVVJfTUlTVFJBTF9LRVlfSEVSRSIsIkdST1FfQVBJX0tFWSI6IllPVVJfR1JPUV9LRVlfSEVSRSIsIk9QRU5ST1VURVJfQVBJX0tFWSI6IllPVVJfT1BFTlJPVVRFUl9LRVlfSEVSRSIsIlhBSV9BUElfS0VZIjoiWU9VUl9YQUlfS0VZX0hFUkUiLCJBWlVSRV9PUEVOQUlfQVBJX0tFWSI6IllPVVJfQVpVUkVfS0VZX0hFUkUiLCJPTExBTUFfQVBJX0tFWSI6IllPVVJfT0xMQU1BX0FQSV9LRVlfSEVSRSJ9fQ%3D%3D)
|
||||
[](https://cursor.com/en/install-mcp?name=task-master-ai&config=eyJjb21tYW5kIjoibnB4IC15IC0tcGFja2FnZT10YXNrLW1hc3Rlci1haSB0YXNrLW1hc3Rlci1haSIsImVudiI6eyJBTlRIUk9QSUNfQVBJX0tFWSI6IllPVVJfQU5USFJPUElDX0FQSV9LRVlfSEVSRSIsIlBFUlBMRVhJVFlfQVBJX0tFWSI6IllPVVJfUEVSUExFWElUWV9BUElfS0VZX0hFUkUiLCJPUEVOQUlfQVBJX0tFWSI6IllPVVJfT1BFTkFJX0tFWV9IRVJFIiwiR09PR0xFX0FQSV9LRVkiOiJZT1VSX0dPT0dMRV9LRVlfSEVSRSIsIk1JU1RSQUxfQVBJX0tFWSI6IllPVVJfTUlTVFJBTF9LRVlfSEVSRSIsIkdST1FfQVBJX0tFWSI6IllPVVJfR1JPUV9LRVlfSEVSRSIsIk9QRU5ST1VURVJfQVBJX0tFWSI6IllPVVJfT1BFTlJPVVRFUl9LRVlfSEVSRSIsIlhBSV9BUElfS0VZIjoiWU9VUl9YQUlfS0VZX0hFUkUiLCJBWlVSRV9PUEVOQUlfQVBJX0tFWSI6IllPVVJfQVpVUkVfS0VZX0hFUkUiLCJPTExBTUFfQVBJX0tFWSI6IllPVVJfT0xMQU1BX0FQSV9LRVlfSEVSRSJ9fQ%3D%3D)
|
||||
|
||||
> **Note:** After clicking the link, you'll still need to add your API keys to the configuration. The link installs the MCP server with placeholder keys that you'll need to replace with your actual API keys.
|
||||
|
||||
|
||||
3
apps/docs/CHANGELOG.md
Normal file
3
apps/docs/CHANGELOG.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# docs
|
||||
|
||||
## 0.0.1
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "docs",
|
||||
"version": "0.0.0",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"description": "Task Master documentation powered by Mintlify",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,43 @@
|
||||
# Change Log
|
||||
|
||||
## 0.24.2-rc.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`8783708`](https://github.com/eyaltoledano/claude-task-master/commit/8783708e5e3389890a78fcf685d3da0580e73b3f), [`37af0f1`](https://github.com/eyaltoledano/claude-task-master/commit/37af0f191227a68d119b7f89a377bf932ee3ac66), [`8783708`](https://github.com/eyaltoledano/claude-task-master/commit/8783708e5e3389890a78fcf685d3da0580e73b3f), [`4dad2fd`](https://github.com/eyaltoledano/claude-task-master/commit/4dad2fd613ceac56a65ae9d3c1c03092b8860ac9)]:
|
||||
- task-master-ai@0.26.0-rc.0
|
||||
|
||||
## 0.24.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`8933557`](https://github.com/eyaltoledano/claude-task-master/commit/89335578ffffc65504b2055c0c85aa7521e5e79b), [`db720a9`](https://github.com/eyaltoledano/claude-task-master/commit/db720a954d390bb44838cd021b8813dde8f3d8de)]:
|
||||
- task-master-ai@0.25.1
|
||||
|
||||
## 0.24.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#1100](https://github.com/eyaltoledano/claude-task-master/pull/1100) [`30ca144`](https://github.com/eyaltoledano/claude-task-master/commit/30ca144231c36a6c63911f20adc225d38fb15a2f) Thanks [@vedovelli](https://github.com/vedovelli)! - Display current task ID on task details page
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`04e11b5`](https://github.com/eyaltoledano/claude-task-master/commit/04e11b5e828597c0ba5b82ca7d5fb6f933e4f1e8), [`fc47714`](https://github.com/eyaltoledano/claude-task-master/commit/fc477143400fd11d953727bf1b4277af5ad308d1), [`782728f`](https://github.com/eyaltoledano/claude-task-master/commit/782728ff95aa2e3b766d48273b57f6c6753e8573), [`3dee60d`](https://github.com/eyaltoledano/claude-task-master/commit/3dee60dc3d566e3cff650accb30f994b8bb3a15e), [`e3ed4d7`](https://github.com/eyaltoledano/claude-task-master/commit/e3ed4d7c14b56894d7da675eb2b757423bea8f9d), [`04e11b5`](https://github.com/eyaltoledano/claude-task-master/commit/04e11b5e828597c0ba5b82ca7d5fb6f933e4f1e8), [`95640dc`](https://github.com/eyaltoledano/claude-task-master/commit/95640dcde87ce7879858c0a951399fb49f3b6397), [`311b243`](https://github.com/eyaltoledano/claude-task-master/commit/311b2433e23c771c8d3a4d3f5ac577302b8321e5)]:
|
||||
- task-master-ai@0.25.0
|
||||
|
||||
## 0.24.0-rc.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#1040](https://github.com/eyaltoledano/claude-task-master/pull/1040) [`fc47714`](https://github.com/eyaltoledano/claude-task-master/commit/fc477143400fd11d953727bf1b4277af5ad308d1) Thanks [@DomVidja](https://github.com/DomVidja)! - "Add Kilo Code profile integration with custom modes and MCP configuration"
|
||||
|
||||
- [#1100](https://github.com/eyaltoledano/claude-task-master/pull/1100) [`30ca144`](https://github.com/eyaltoledano/claude-task-master/commit/30ca144231c36a6c63911f20adc225d38fb15a2f) Thanks [@vedovelli](https://github.com/vedovelli)! - Display current task ID on task details page
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`04e11b5`](https://github.com/eyaltoledano/claude-task-master/commit/04e11b5e828597c0ba5b82ca7d5fb6f933e4f1e8), [`fc47714`](https://github.com/eyaltoledano/claude-task-master/commit/fc477143400fd11d953727bf1b4277af5ad308d1), [`782728f`](https://github.com/eyaltoledano/claude-task-master/commit/782728ff95aa2e3b766d48273b57f6c6753e8573), [`3dee60d`](https://github.com/eyaltoledano/claude-task-master/commit/3dee60dc3d566e3cff650accb30f994b8bb3a15e), [`e3ed4d7`](https://github.com/eyaltoledano/claude-task-master/commit/e3ed4d7c14b56894d7da675eb2b757423bea8f9d), [`04e11b5`](https://github.com/eyaltoledano/claude-task-master/commit/04e11b5e828597c0ba5b82ca7d5fb6f933e4f1e8), [`95640dc`](https://github.com/eyaltoledano/claude-task-master/commit/95640dcde87ce7879858c0a951399fb49f3b6397), [`311b243`](https://github.com/eyaltoledano/claude-task-master/commit/311b2433e23c771c8d3a4d3f5ac577302b8321e5)]:
|
||||
- task-master-ai@0.25.0-rc.0
|
||||
|
||||
## 0.23.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"private": true,
|
||||
"displayName": "TaskMaster",
|
||||
"description": "A visual Kanban board interface for TaskMaster projects in VS Code",
|
||||
"version": "0.23.1",
|
||||
"version": "0.24.2-rc.0",
|
||||
"publisher": "Hamster",
|
||||
"icon": "assets/icon.png",
|
||||
"engines": {
|
||||
@@ -239,7 +239,7 @@
|
||||
"check-types": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"task-master-ai": "0.24.0"
|
||||
"task-master-ai": "0.26.0-rc.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
|
||||
@@ -65,11 +65,10 @@ This removes the dependency relationships and moves only the specified task.
|
||||
|
||||
### Force Move
|
||||
|
||||
Force the move even with dependency conflicts:
|
||||
Note: Force moves are no longer supported. Instead, use one of these options:
|
||||
|
||||
```bash
|
||||
task-master move --from=5 --from-tag=backlog --to-tag=in-progress --force
|
||||
```
|
||||
- `--with-dependencies` — move dependents together
|
||||
- `--ignore-dependencies` — break cross-tag dependencies
|
||||
|
||||
⚠️ **Warning**: This may break dependency relationships and should be used with caution.
|
||||
|
||||
@@ -93,7 +92,7 @@ Resolution options:
|
||||
2. Break dependencies: task-master move --from=5,6 --from-tag=backlog --to-tag=in-progress --ignore-dependencies
|
||||
3. Validate and fix dependencies: task-master validate-dependencies && task-master fix-dependencies
|
||||
4. Move dependencies first: task-master move --from=2,3 --from-tag=backlog --to-tag=in-progress
|
||||
5. Force move (may break dependencies): task-master move --from=5,6 --from-tag=backlog --to-tag=in-progress --force
|
||||
5. After deciding, re-run the move with either --with-dependencies or --ignore-dependencies
|
||||
```
|
||||
|
||||
### Subtask Movement Restrictions
|
||||
@@ -149,7 +148,6 @@ task-master fix-dependencies
|
||||
|
||||
- **`--with-dependencies`**: When you want to maintain task relationships
|
||||
- **`--ignore-dependencies`**: When you want to break cross-tag dependencies
|
||||
- **`--force`**: Only when you understand the consequences
|
||||
|
||||
### 3. Organize by Context
|
||||
|
||||
@@ -265,9 +263,14 @@ task-master move --from=5 --from-tag=backlog --to-tag=done --ignore-dependencies
|
||||
|
||||
### Scenario 3: Force Move
|
||||
|
||||
Choose one of these options explicitly:
|
||||
|
||||
```bash
|
||||
# Force move despite conflicts
|
||||
task-master move --from=5 --from-tag=backlog --to-tag=in-progress --force
|
||||
# Move together with dependencies
|
||||
task-master move --from=5 --from-tag=backlog --to-tag=in-progress --with-dependencies
|
||||
|
||||
# Or break dependencies
|
||||
task-master move --from=5 --from-tag=backlog --to-tag=in-progress --ignore-dependencies
|
||||
```
|
||||
|
||||
### Scenario 4: Moving Subtasks
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Available Models as of August 11, 2025
|
||||
# Available Models as of August 12, 2025
|
||||
|
||||
## Main Models
|
||||
|
||||
@@ -68,6 +68,9 @@
|
||||
| openrouter | mistralai/mistral-small-3.1-24b-instruct | — | 0.1 | 0.3 |
|
||||
| openrouter | mistralai/devstral-small | — | 0.1 | 0.3 |
|
||||
| openrouter | mistralai/mistral-nemo | — | 0.03 | 0.07 |
|
||||
| ollama | gpt-oss:latest | 0.607 | 0 | 0 |
|
||||
| ollama | gpt-oss:20b | 0.607 | 0 | 0 |
|
||||
| ollama | gpt-oss:120b | 0.624 | 0 | 0 |
|
||||
| ollama | devstral:latest | — | 0 | 0 |
|
||||
| ollama | qwen3:latest | — | 0 | 0 |
|
||||
| ollama | qwen3:14b | — | 0 | 0 |
|
||||
@@ -174,6 +177,9 @@
|
||||
| openrouter | qwen/qwen3-235b-a22b | — | 0.14 | 2 |
|
||||
| openrouter | mistralai/mistral-small-3.1-24b-instruct | — | 0.1 | 0.3 |
|
||||
| openrouter | mistralai/mistral-nemo | — | 0.03 | 0.07 |
|
||||
| ollama | gpt-oss:latest | 0.607 | 0 | 0 |
|
||||
| ollama | gpt-oss:20b | 0.607 | 0 | 0 |
|
||||
| ollama | gpt-oss:120b | 0.624 | 0 | 0 |
|
||||
| ollama | devstral:latest | — | 0 | 0 |
|
||||
| ollama | qwen3:latest | — | 0 | 0 |
|
||||
| ollama | qwen3:14b | — | 0 | 0 |
|
||||
|
||||
@@ -189,6 +189,17 @@ export async function moveTaskCrossTagDirect(args, log, context = {}) {
|
||||
'Verify task IDs exist: task-master list',
|
||||
'Check task details: task-master show <id>'
|
||||
];
|
||||
} else if (
|
||||
error.code === 'TASK_ALREADY_EXISTS' ||
|
||||
error.message?.includes('already exists in target tag')
|
||||
) {
|
||||
// Target tag has an ID collision
|
||||
errorCode = 'TASK_ALREADY_EXISTS';
|
||||
suggestions = [
|
||||
'Choose a different target tag without conflicting IDs',
|
||||
'Move a different set of IDs (avoid existing ones)',
|
||||
'If needed, move within-tag to a new ID first, then cross-tag move'
|
||||
];
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -32,7 +32,7 @@ import { TASKMASTER_TASKS_FILE } from '../../../../src/constants/paths.js';
|
||||
* @returns {Promise<Object>} - Result object with success status and data/error information.
|
||||
*/
|
||||
export async function parsePRDDirect(args, log, context = {}) {
|
||||
const { session } = context;
|
||||
const { session, reportProgress } = context;
|
||||
// Extract projectRoot from args
|
||||
const {
|
||||
input: inputArg,
|
||||
@@ -164,6 +164,7 @@ export async function parsePRDDirect(args, log, context = {}) {
|
||||
force,
|
||||
append,
|
||||
research,
|
||||
reportProgress,
|
||||
commandName: 'parse-prd',
|
||||
outputType: 'mcp'
|
||||
},
|
||||
|
||||
@@ -7,7 +7,8 @@ import { z } from 'zod';
|
||||
import {
|
||||
handleApiResult,
|
||||
withNormalizedProjectRoot,
|
||||
createErrorResponse
|
||||
createErrorResponse,
|
||||
checkProgressCapability
|
||||
} from './utils.js';
|
||||
import { parsePRDDirect } from '../core/task-master-core.js';
|
||||
import {
|
||||
@@ -64,31 +65,37 @@ export function registerParsePRDTool(server) {
|
||||
.optional()
|
||||
.describe('Append generated tasks to existing file.')
|
||||
}),
|
||||
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
|
||||
try {
|
||||
const resolvedTag = resolveTag({
|
||||
projectRoot: args.projectRoot,
|
||||
tag: args.tag
|
||||
});
|
||||
const result = await parsePRDDirect(
|
||||
{
|
||||
...args,
|
||||
tag: resolvedTag
|
||||
},
|
||||
log,
|
||||
{ session }
|
||||
);
|
||||
return handleApiResult(
|
||||
result,
|
||||
log,
|
||||
'Error parsing PRD',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(`Error in parse_prd: ${error.message}`);
|
||||
return createErrorResponse(`Failed to parse PRD: ${error.message}`);
|
||||
execute: withNormalizedProjectRoot(
|
||||
async (args, { log, session, reportProgress }) => {
|
||||
try {
|
||||
const resolvedTag = resolveTag({
|
||||
projectRoot: args.projectRoot,
|
||||
tag: args.tag
|
||||
});
|
||||
const progressCapability = checkProgressCapability(
|
||||
reportProgress,
|
||||
log
|
||||
);
|
||||
const result = await parsePRDDirect(
|
||||
{
|
||||
...args,
|
||||
tag: resolvedTag
|
||||
},
|
||||
log,
|
||||
{ session, reportProgress: progressCapability }
|
||||
);
|
||||
return handleApiResult(
|
||||
result,
|
||||
log,
|
||||
'Error parsing PRD',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(`Error in parse_prd: ${error.message}`);
|
||||
return createErrorResponse(`Failed to parse PRD: ${error.message}`);
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -778,6 +778,77 @@ function withNormalizedProjectRoot(executeFn) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks progress reporting capability and returns the validated function or undefined.
|
||||
*
|
||||
* STANDARD PATTERN for AI-powered, long-running operations (parse-prd, expand-task, expand-all, analyze):
|
||||
*
|
||||
* This helper should be used as the first step in any MCP tool that performs long-running
|
||||
* AI operations. It validates the availability of progress reporting and provides consistent
|
||||
* logging about the capability status.
|
||||
*
|
||||
* Operations that should use this pattern:
|
||||
* - parse-prd: Parsing PRD documents with AI
|
||||
* - expand-task: Expanding tasks into subtasks
|
||||
* - expand-all: Expanding all tasks in batch
|
||||
* - analyze-complexity: Analyzing task complexity
|
||||
* - update-task: Updating tasks with AI assistance
|
||||
* - add-task: Creating new tasks with AI
|
||||
* - Any operation that makes AI service calls
|
||||
*
|
||||
* @example Basic usage in a tool's execute function:
|
||||
* ```javascript
|
||||
* import { checkProgressCapability } from './utils.js';
|
||||
*
|
||||
* async execute(args, context) {
|
||||
* const { log, reportProgress, session } = context;
|
||||
*
|
||||
* // Always validate progress capability first
|
||||
* const progressCapability = checkProgressCapability(reportProgress, log);
|
||||
*
|
||||
* // Pass to direct function - it handles undefined gracefully
|
||||
* const result = await expandTask(taskId, numSubtasks, {
|
||||
* session,
|
||||
* reportProgress: progressCapability,
|
||||
* mcpLog: log
|
||||
* });
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @example With progress reporting available:
|
||||
* ```javascript
|
||||
* // When reportProgress is available, users see real-time updates:
|
||||
* // "Starting PRD analysis (Input: 5432 tokens)..."
|
||||
* // "Task 1/10 - Implement user authentication"
|
||||
* // "Task 2/10 - Create database schema"
|
||||
* // "Task Generation Completed | Tokens: 5432/1234"
|
||||
* ```
|
||||
*
|
||||
* @example Without progress reporting (graceful degradation):
|
||||
* ```javascript
|
||||
* // When reportProgress is not available:
|
||||
* // - Operation runs normally without progress updates
|
||||
* // - Debug log: "reportProgress not available - operation will run without progress updates"
|
||||
* // - User gets final result after completion
|
||||
* ```
|
||||
*
|
||||
* @param {Function|undefined} reportProgress - The reportProgress function from MCP context.
|
||||
* Expected signature: async (progress: {progress: number, total: number, message: string}) => void
|
||||
* @param {Object} log - Logger instance with debug, info, warn, error methods
|
||||
* @returns {Function|undefined} The validated reportProgress function or undefined if not available
|
||||
*/
|
||||
function checkProgressCapability(reportProgress, log) {
|
||||
// Validate that reportProgress is available for long-running operations
|
||||
if (typeof reportProgress !== 'function') {
|
||||
log.debug(
|
||||
'reportProgress not available - operation will run without progress updates'
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return reportProgress;
|
||||
}
|
||||
|
||||
// Ensure all functions are exported
|
||||
export {
|
||||
getProjectRoot,
|
||||
@@ -792,5 +863,6 @@ export {
|
||||
createLogWrapper,
|
||||
normalizeProjectRoot,
|
||||
getRawProjectRootFromSession,
|
||||
withNormalizedProjectRoot
|
||||
withNormalizedProjectRoot,
|
||||
checkProgressCapability
|
||||
};
|
||||
|
||||
729
package-lock.json
generated
729
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "task-master-ai",
|
||||
"version": "0.24.0",
|
||||
"version": "0.25.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "task-master-ai",
|
||||
"version": "0.24.0",
|
||||
"version": "0.25.1",
|
||||
"license": "MIT WITH Commons-Clause",
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
@@ -27,12 +27,14 @@
|
||||
"@aws-sdk/credential-providers": "^3.817.0",
|
||||
"@inquirer/search": "^3.0.15",
|
||||
"@openrouter/ai-sdk-provider": "^0.4.5",
|
||||
"@streamparser/json": "^0.0.22",
|
||||
"ai": "^4.3.10",
|
||||
"ajv": "^8.17.1",
|
||||
"ajv-formats": "^3.0.1",
|
||||
"boxen": "^8.0.1",
|
||||
"chalk": "^5.4.1",
|
||||
"cli-highlight": "^2.1.11",
|
||||
"cli-progress": "^3.12.0",
|
||||
"cli-table3": "^0.6.5",
|
||||
"commander": "^11.1.0",
|
||||
"cors": "^2.8.5",
|
||||
@@ -79,21 +81,21 @@
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@anthropic-ai/claude-code": "^1.0.25",
|
||||
"@anthropic-ai/claude-code": "^1.0.88",
|
||||
"@biomejs/cli-linux-x64": "^1.9.4",
|
||||
"ai-sdk-provider-gemini-cli": "^0.1.1"
|
||||
"ai-sdk-provider-gemini-cli": "^0.1.3"
|
||||
}
|
||||
},
|
||||
"apps/docs": {
|
||||
"version": "0.0.0",
|
||||
"version": "0.0.1",
|
||||
"devDependencies": {
|
||||
"mintlify": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"apps/extension": {
|
||||
"version": "0.23.1",
|
||||
"version": "0.24.1",
|
||||
"dependencies": {
|
||||
"task-master-ai": "0.24.0"
|
||||
"task-master-ai": "0.25.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
@@ -2044,10 +2046,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-code": {
|
||||
"version": "1.0.34",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code/-/claude-code-1.0.34.tgz",
|
||||
"integrity": "sha512-9mQd8hodE5/RxZnsWUCdLzqGUKuCzBczrfc2QfxrNSlvUFpOgTzjT1Zlww2vW9v0K1e5K9g1o08apqPl/QPmpw==",
|
||||
"hasInstallScript": true,
|
||||
"version": "1.0.88",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code/-/claude-code-1.0.88.tgz",
|
||||
"integrity": "sha512-Np6H4EjkbmNolUpx98DvqLXV/iJrw2y7dz2rDJ7av9ajMz6HZfB8bdJV5D75+jO+Gk1pvA54HCIm0c65lDrzcw==",
|
||||
"license": "SEE LICENSE IN README.md",
|
||||
"optional": true,
|
||||
"bin": {
|
||||
@@ -4072,31 +4073,40 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@google/gemini-cli-core": {
|
||||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@google/gemini-cli-core/-/gemini-cli-core-0.1.13.tgz",
|
||||
"integrity": "sha512-Vx3CbRpLJiGs/sj4SXlGH2ALKyON5skV/p+SCAoRuS6yRsANS1+diEeXbp6jlWT2TTiGoa8+GolqeNIU7wbN8w==",
|
||||
"version": "0.1.22",
|
||||
"resolved": "https://registry.npmjs.org/@google/gemini-cli-core/-/gemini-cli-core-0.1.22.tgz",
|
||||
"integrity": "sha512-PvIod0b8+vB8Wfdpr4axiDopL6sxeQ/4qF4Q1zdXOD3ANUYSLuhq3Af8OrugA0so6vCjDMllyKcnOmGT6s6+ag==",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@google/genai": "1.9.0",
|
||||
"@google/genai": "1.13.0",
|
||||
"@modelcontextprotocol/sdk": "^1.11.0",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/exporter-logs-otlp-grpc": "^0.52.0",
|
||||
"@opentelemetry/exporter-logs-otlp-http": "^0.52.0",
|
||||
"@opentelemetry/exporter-metrics-otlp-grpc": "^0.52.0",
|
||||
"@opentelemetry/exporter-metrics-otlp-http": "^0.52.0",
|
||||
"@opentelemetry/exporter-trace-otlp-grpc": "^0.52.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.52.0",
|
||||
"@opentelemetry/instrumentation-http": "^0.52.0",
|
||||
"@opentelemetry/sdk-node": "^0.52.0",
|
||||
"@types/glob": "^8.1.0",
|
||||
"@types/html-to-text": "^9.0.4",
|
||||
"ajv": "^8.17.1",
|
||||
"chardet": "^2.1.0",
|
||||
"diff": "^7.0.0",
|
||||
"dotenv": "^17.1.0",
|
||||
"fdir": "^6.4.6",
|
||||
"fzf": "^0.5.2",
|
||||
"glob": "^10.4.5",
|
||||
"google-auth-library": "^9.11.0",
|
||||
"html-to-text": "^9.0.5",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"ignore": "^7.0.0",
|
||||
"marked": "^15.0.12",
|
||||
"micromatch": "^4.0.8",
|
||||
"mnemonist": "^0.40.3",
|
||||
"open": "^10.1.2",
|
||||
"picomatch": "^4.0.1",
|
||||
"shell-quote": "^1.8.3",
|
||||
"simple-git": "^3.28.0",
|
||||
"strip-ansi": "^7.1.0",
|
||||
@@ -4108,9 +4118,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@google/gemini-cli-core/node_modules/@google/genai": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.9.0.tgz",
|
||||
"integrity": "sha512-w9P93OXKPMs9H1mfAx9+p3zJqQGrWBGdvK/SVc7cLZEXNHr/3+vW2eif7ZShA6wU24rNLn9z9MK2vQFUvNRI2Q==",
|
||||
"version": "1.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.13.0.tgz",
|
||||
"integrity": "sha512-BxilXzE8cJ0zt5/lXk6KwuBcIT9P2Lbi2WXhwWMbxf1RNeC68/8DmYQqMrzQP333CieRMdbDXs0eNCphLoScWg==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -4130,9 +4140,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@google/gemini-cli-core/node_modules/ansi-regex": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
|
||||
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz",
|
||||
"integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
@@ -4152,10 +4162,17 @@
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@google/gemini-cli-core/node_modules/chardet": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.0.tgz",
|
||||
"integrity": "sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@google/gemini-cli-core/node_modules/dotenv": {
|
||||
"version": "17.2.0",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.0.tgz",
|
||||
"integrity": "sha512-Q4sgBT60gzd0BB0lSyYD3xM4YxrXA9y4uBDof1JNYGzOXrQdQ6yX+7XIAqoFOGQFOTK1D3Hts5OllpxMDZFONQ==",
|
||||
"version": "17.2.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz",
|
||||
"integrity": "sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==",
|
||||
"license": "BSD-2-Clause",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
@@ -4165,6 +4182,24 @@
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/@google/gemini-cli-core/node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"picomatch": "^3 || ^4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"picomatch": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@google/gemini-cli-core/node_modules/glob": {
|
||||
"version": "10.4.5",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
|
||||
@@ -4212,6 +4247,19 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/@google/gemini-cli-core/node_modules/picomatch": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/@google/gemini-cli-core/node_modules/strip-ansi": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
|
||||
@@ -4229,9 +4277,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@google/genai": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.10.0.tgz",
|
||||
"integrity": "sha512-PR4tLuiIFMrpAiiCko2Z16ydikFsPF1c5TBfI64hlZcv3xBEApSCceLuDYu1pNMq2SkNh4r66J4AG+ZexBnMLw==",
|
||||
"version": "1.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.14.0.tgz",
|
||||
"integrity": "sha512-jirYprAAJU1svjwSDVCzyVq+FrJpJd5CSxR/g2Ga/gZ0ZYZpcWjMS75KJl9y71K1mDN+tcx6s21CzCbB2R840g==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -7736,6 +7784,26 @@
|
||||
"@opentelemetry/api": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/exporter-logs-otlp-http": {
|
||||
"version": "0.52.1",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.52.1.tgz",
|
||||
"integrity": "sha512-qKgywId2DbdowPZpOBXQKp0B8DfhfIArmSic15z13Nk/JAOccBUQdPwDjDnjsM5f0ckZFMVR2t/tijTUAqDZoA==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/api-logs": "0.52.1",
|
||||
"@opentelemetry/core": "1.25.1",
|
||||
"@opentelemetry/otlp-exporter-base": "0.52.1",
|
||||
"@opentelemetry/otlp-transformer": "0.52.1",
|
||||
"@opentelemetry/sdk-logs": "0.52.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/exporter-metrics-otlp-grpc": {
|
||||
"version": "0.52.1",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.52.1.tgz",
|
||||
@@ -9622,6 +9690,12 @@
|
||||
"node": "^12.20 || >=14.13"
|
||||
}
|
||||
},
|
||||
"node_modules/@streamparser/json": {
|
||||
"version": "0.0.22",
|
||||
"resolved": "https://registry.npmjs.org/@streamparser/json/-/json-0.0.22.tgz",
|
||||
"integrity": "sha512-b6gTSBjJ8G8SuO3Gbbj+zXbVx8NSs1EbpbMKpzGLWMdkR+98McH9bEjSz3+0mPJf68c5nxa3CrJHp5EQNXM6zQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@szmarczak/http-timer": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz",
|
||||
@@ -9680,23 +9754,6 @@
|
||||
"@tailwindcss/oxide-win32-x64-msvc": "4.1.11"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-android-arm64": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.11.tgz",
|
||||
"integrity": "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-darwin-arm64": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.11.tgz",
|
||||
@@ -9714,189 +9771,6 @@
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-darwin-x64": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.11.tgz",
|
||||
"integrity": "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-freebsd-x64": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.11.tgz",
|
||||
"integrity": "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.11.tgz",
|
||||
"integrity": "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.11.tgz",
|
||||
"integrity": "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.11.tgz",
|
||||
"integrity": "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.11.tgz",
|
||||
"integrity": "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.11.tgz",
|
||||
"integrity": "sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.11.tgz",
|
||||
"integrity": "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==",
|
||||
"bundleDependencies": [
|
||||
"@napi-rs/wasm-runtime",
|
||||
"@emnapi/core",
|
||||
"@emnapi/runtime",
|
||||
"@tybys/wasm-util",
|
||||
"@emnapi/wasi-threads",
|
||||
"tslib"
|
||||
],
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "^1.4.3",
|
||||
"@emnapi/runtime": "^1.4.3",
|
||||
"@emnapi/wasi-threads": "^1.0.2",
|
||||
"@napi-rs/wasm-runtime": "^0.2.11",
|
||||
"@tybys/wasm-util": "^0.9.0",
|
||||
"tslib": "^2.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.11.tgz",
|
||||
"integrity": "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.11.tgz",
|
||||
"integrity": "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/postcss": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.11.tgz",
|
||||
@@ -10638,34 +10512,6 @@
|
||||
"@vscode/vsce-sign-win32-x64": "2.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@vscode/vsce-sign-alpine-arm64": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-arm64/-/vsce-sign-alpine-arm64-2.0.5.tgz",
|
||||
"integrity": "sha512-XVmnF40APwRPXSLYA28Ye+qWxB25KhSVpF2eZVtVOs6g7fkpOxsVnpRU1Bz2xG4ySI79IRuapDJoAQFkoOgfdQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "SEE LICENSE IN LICENSE.txt",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"alpine"
|
||||
]
|
||||
},
|
||||
"node_modules/@vscode/vsce-sign-alpine-x64": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-x64/-/vsce-sign-alpine-x64-2.0.5.tgz",
|
||||
"integrity": "sha512-JuxY3xcquRsOezKq6PEHwCgd1rh1GnhyH6urVEWUzWn1c1PC4EOoyffMD+zLZtFuZF5qR1I0+cqDRNKyPvpK7Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "SEE LICENSE IN LICENSE.txt",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"alpine"
|
||||
]
|
||||
},
|
||||
"node_modules/@vscode/vsce-sign-darwin-arm64": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-arm64/-/vsce-sign-darwin-arm64-2.0.5.tgz",
|
||||
@@ -10680,90 +10526,6 @@
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@vscode/vsce-sign-darwin-x64": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-x64/-/vsce-sign-darwin-x64-2.0.5.tgz",
|
||||
"integrity": "sha512-ma9JDC7FJ16SuPXlLKkvOD2qLsmW/cKfqK4zzM2iJE1PbckF3BlR08lYqHV89gmuoTpYB55+z8Y5Fz4wEJBVDA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "SEE LICENSE IN LICENSE.txt",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@vscode/vsce-sign-linux-arm": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm/-/vsce-sign-linux-arm-2.0.5.tgz",
|
||||
"integrity": "sha512-cdCwtLGmvC1QVrkIsyzv01+o9eR+wodMJUZ9Ak3owhcGxPRB53/WvrDHAFYA6i8Oy232nuen1YqWeEohqBuSzA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "SEE LICENSE IN LICENSE.txt",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@vscode/vsce-sign-linux-arm64": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm64/-/vsce-sign-linux-arm64-2.0.5.tgz",
|
||||
"integrity": "sha512-Hr1o0veBymg9SmkCqYnfaiUnes5YK6k/lKFA5MhNmiEN5fNqxyPUCdRZMFs3Ajtx2OFW4q3KuYVRwGA7jdLo7Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "SEE LICENSE IN LICENSE.txt",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@vscode/vsce-sign-linux-x64": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-x64/-/vsce-sign-linux-x64-2.0.5.tgz",
|
||||
"integrity": "sha512-XLT0gfGMcxk6CMRLDkgqEPTyG8Oa0OFe1tPv2RVbphSOjFWJwZgK3TYWx39i/7gqpDHlax0AP6cgMygNJrA6zg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "SEE LICENSE IN LICENSE.txt",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@vscode/vsce-sign-win32-arm64": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-arm64/-/vsce-sign-win32-arm64-2.0.5.tgz",
|
||||
"integrity": "sha512-hco8eaoTcvtmuPhavyCZhrk5QIcLiyAUhEso87ApAWDllG7djIrWiOCtqn48k4pHz+L8oCQlE0nwNHfcYcxOPw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "SEE LICENSE IN LICENSE.txt",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@vscode/vsce-sign-win32-x64": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-x64/-/vsce-sign-win32-x64-2.0.5.tgz",
|
||||
"integrity": "sha512-1ixKFGM2FwM+6kQS2ojfY3aAelICxjiCzeg4nTHpkeU1Tfs4RC+lVLrgq5NwcBC7ZLr6UfY3Ct3D6suPeOf7BQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "SEE LICENSE IN LICENSE.txt",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@vscode/vsce/node_modules/ansi-styles": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
|
||||
@@ -10995,15 +10757,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ai-sdk-provider-gemini-cli": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ai-sdk-provider-gemini-cli/-/ai-sdk-provider-gemini-cli-0.1.1.tgz",
|
||||
"integrity": "sha512-fvX3n9jTt8JaTyc+qDv5Og0H4NQMpS6B1VdaTT71AN2F+3u2Bz9/OSd7ATokrV2Rmv+ZlEnUCmJnke58zHXUSQ==",
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ai-sdk-provider-gemini-cli/-/ai-sdk-provider-gemini-cli-0.1.3.tgz",
|
||||
"integrity": "sha512-hNsp2YIDLr+nVqxk8l6UzRozj3e1sfh3nzjyNSHB4f47dMBReb9qTq/1GwOlpKGEPcLK7OoibbX9wFC/eWBlvA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "^1.1.3",
|
||||
"@ai-sdk/provider-utils": "^2.2.8",
|
||||
"@google/gemini-cli-core": "^0.1.13",
|
||||
"@google/gemini-cli-core": "0.1.22",
|
||||
"@google/genai": "^1.7.0",
|
||||
"google-auth-library": "^9.0.0",
|
||||
"zod": "^3.23.8",
|
||||
@@ -12883,6 +12645,47 @@
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-progress": {
|
||||
"version": "3.12.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz",
|
||||
"integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-progress/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cli-progress/node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-progress/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-spinners": {
|
||||
"version": "2.9.2",
|
||||
"resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz",
|
||||
@@ -14781,12 +14584,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eventsource-parser": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.2.tgz",
|
||||
"integrity": "sha512-6RxOBZ/cYgd8usLwsEl+EC09Au/9BcmCKYF2/xbml6DNczf7nv0MQb+7BA2F+li6//I+28VNlQR37XfQtcAJuA==",
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.5.tgz",
|
||||
"integrity": "sha512-bSRG85ZrMdmWtm7qkF9He9TNRzc/Bm99gEJMaQoHJ9E6Kv9QBbsldh2oMj7iXmYNEAVvNgvv5vPorG6W+XtBhQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/execa": {
|
||||
@@ -15770,6 +15573,13 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/fzf": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/fzf/-/fzf-0.5.2.tgz",
|
||||
"integrity": "sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q==",
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/gaxios": {
|
||||
"version": "6.7.1",
|
||||
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz",
|
||||
@@ -19956,195 +19766,6 @@
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-darwin-x64": {
|
||||
"version": "1.30.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz",
|
||||
"integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-freebsd-x64": {
|
||||
"version": "1.30.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz",
|
||||
"integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm-gnueabihf": {
|
||||
"version": "1.30.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz",
|
||||
"integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm64-gnu": {
|
||||
"version": "1.30.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz",
|
||||
"integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm64-musl": {
|
||||
"version": "1.30.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz",
|
||||
"integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-x64-gnu": {
|
||||
"version": "1.30.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz",
|
||||
"integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-x64-musl": {
|
||||
"version": "1.30.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz",
|
||||
"integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-win32-arm64-msvc": {
|
||||
"version": "1.30.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz",
|
||||
"integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-win32-x64-msvc": {
|
||||
"version": "1.30.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz",
|
||||
"integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lilconfig": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
|
||||
@@ -20493,6 +20114,19 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "15.0.12",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz",
|
||||
"integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
@@ -22042,6 +21676,16 @@
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/mnemonist": {
|
||||
"version": "0.40.3",
|
||||
"resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.40.3.tgz",
|
||||
"integrity": "sha512-Vjyr90sJ23CKKH/qPAgUKicw/v6pRoamxIEDFOF8uSgFME7DqPRpHgRTejWVjkdGg5dXj0/NyxZHZ9bcjH+2uQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"obliterator": "^2.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/mocha": {
|
||||
"version": "11.7.1",
|
||||
"resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.1.tgz",
|
||||
@@ -23063,6 +22707,13 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/obliterator": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.5.tgz",
|
||||
"integrity": "sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/ollama-ai-provider": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ollama-ai-provider/-/ollama-ai-provider-1.2.0.tgz",
|
||||
@@ -24202,9 +23853,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/protobufjs": {
|
||||
"version": "7.5.3",
|
||||
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.3.tgz",
|
||||
"integrity": "sha512-sildjKwVqOI2kmFDiXQ6aEB0fjYTafpEvIBs8tOR8qI4spuL9OPROLVu2qZqi/xgCfsHIwVqlaF8JBjWFHnKbw==",
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz",
|
||||
"integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "task-master-ai",
|
||||
"version": "0.24.0",
|
||||
"version": "0.26.0-rc.0",
|
||||
"description": "A task management system for ambitious AI-driven development that doesn't overwhelm and confuse Cursor.",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
@@ -54,12 +54,14 @@
|
||||
"@aws-sdk/credential-providers": "^3.817.0",
|
||||
"@inquirer/search": "^3.0.15",
|
||||
"@openrouter/ai-sdk-provider": "^0.4.5",
|
||||
"@streamparser/json": "^0.0.22",
|
||||
"ai": "^4.3.10",
|
||||
"ajv": "^8.17.1",
|
||||
"ajv-formats": "^3.0.1",
|
||||
"boxen": "^8.0.1",
|
||||
"chalk": "^5.4.1",
|
||||
"cli-highlight": "^2.1.11",
|
||||
"cli-progress": "^3.12.0",
|
||||
"cli-table3": "^0.6.5",
|
||||
"commander": "^11.1.0",
|
||||
"cors": "^2.8.5",
|
||||
@@ -84,9 +86,9 @@
|
||||
"zod-to-json-schema": "^3.24.5"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@anthropic-ai/claude-code": "^1.0.25",
|
||||
"@anthropic-ai/claude-code": "^1.0.88",
|
||||
"@biomejs/cli-linux-x64": "^1.9.4",
|
||||
"ai-sdk-provider-gemini-cli": "^0.1.1"
|
||||
"ai-sdk-provider-gemini-cli": "^0.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
|
||||
@@ -91,93 +91,117 @@ function _getProvider(providerName) {
|
||||
|
||||
// Helper function to get cost for a specific model
|
||||
function _getCostForModel(providerName, modelId) {
|
||||
const DEFAULT_COST = { inputCost: 0, outputCost: 0, currency: 'USD' };
|
||||
|
||||
if (!MODEL_MAP || !MODEL_MAP[providerName]) {
|
||||
log(
|
||||
'warn',
|
||||
`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 DEFAULT_COST;
|
||||
}
|
||||
|
||||
const modelData = MODEL_MAP[providerName].find((m) => m.id === modelId);
|
||||
|
||||
if (!modelData || !modelData.cost_per_1m_tokens) {
|
||||
if (!modelData?.cost_per_1m_tokens) {
|
||||
log(
|
||||
'debug',
|
||||
`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 DEFAULT_COST;
|
||||
}
|
||||
|
||||
// Ensure currency is part of the returned object, defaulting if not present
|
||||
const currency = modelData.cost_per_1m_tokens.currency || 'USD';
|
||||
|
||||
const costs = modelData.cost_per_1m_tokens;
|
||||
return {
|
||||
inputCost: modelData.cost_per_1m_tokens.input || 0,
|
||||
outputCost: modelData.cost_per_1m_tokens.output || 0,
|
||||
currency: currency
|
||||
inputCost: costs.input || 0,
|
||||
outputCost: costs.output || 0,
|
||||
currency: costs.currency || 'USD'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate cost from token counts and cost per million
|
||||
* @param {number} inputTokens - Number of input tokens
|
||||
* @param {number} outputTokens - Number of output tokens
|
||||
* @param {number} inputCost - Cost per million input tokens
|
||||
* @param {number} outputCost - Cost per million output tokens
|
||||
* @returns {number} Total calculated cost
|
||||
*/
|
||||
function _calculateCost(inputTokens, outputTokens, inputCost, outputCost) {
|
||||
const calculatedCost =
|
||||
((inputTokens || 0) / 1_000_000) * inputCost +
|
||||
((outputTokens || 0) / 1_000_000) * outputCost;
|
||||
return parseFloat(calculatedCost.toFixed(6));
|
||||
}
|
||||
|
||||
// Helper function to get tag information for responses
|
||||
function _getTagInfo(projectRoot) {
|
||||
const DEFAULT_TAG_INFO = { currentTag: 'master', availableTags: ['master'] };
|
||||
|
||||
try {
|
||||
if (!projectRoot) {
|
||||
return { currentTag: 'master', availableTags: ['master'] };
|
||||
return DEFAULT_TAG_INFO;
|
||||
}
|
||||
|
||||
const currentTag = getCurrentTag(projectRoot);
|
||||
const currentTag = getCurrentTag(projectRoot) || 'master';
|
||||
const availableTags = _readAvailableTags(projectRoot);
|
||||
|
||||
// Read available tags from tasks.json
|
||||
let availableTags = ['master']; // Default fallback
|
||||
try {
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const tasksPath = path.join(
|
||||
projectRoot,
|
||||
'.taskmaster',
|
||||
'tasks',
|
||||
'tasks.json'
|
||||
);
|
||||
|
||||
if (fs.existsSync(tasksPath)) {
|
||||
const tasksData = JSON.parse(fs.readFileSync(tasksPath, 'utf8'));
|
||||
if (tasksData && typeof tasksData === 'object') {
|
||||
// Check if it's tagged format (has tag-like keys with tasks arrays)
|
||||
const potentialTags = Object.keys(tasksData).filter(
|
||||
(key) =>
|
||||
tasksData[key] &&
|
||||
typeof tasksData[key] === 'object' &&
|
||||
Array.isArray(tasksData[key].tasks)
|
||||
);
|
||||
|
||||
if (potentialTags.length > 0) {
|
||||
availableTags = potentialTags;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (readError) {
|
||||
// Silently fall back to default if we can't read tasks file
|
||||
if (getDebugFlag()) {
|
||||
log(
|
||||
'debug',
|
||||
`Could not read tasks file for available tags: ${readError.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
currentTag: currentTag || 'master',
|
||||
availableTags: availableTags
|
||||
};
|
||||
return { currentTag, availableTags };
|
||||
} catch (error) {
|
||||
if (getDebugFlag()) {
|
||||
log('debug', `Error getting tag information: ${error.message}`);
|
||||
}
|
||||
return { currentTag: 'master', availableTags: ['master'] };
|
||||
return DEFAULT_TAG_INFO;
|
||||
}
|
||||
}
|
||||
|
||||
// Extract method for reading available tags
|
||||
function _readAvailableTags(projectRoot) {
|
||||
const DEFAULT_TAGS = ['master'];
|
||||
|
||||
try {
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const tasksPath = path.join(
|
||||
projectRoot,
|
||||
'.taskmaster',
|
||||
'tasks',
|
||||
'tasks.json'
|
||||
);
|
||||
|
||||
if (!fs.existsSync(tasksPath)) {
|
||||
return DEFAULT_TAGS;
|
||||
}
|
||||
|
||||
const tasksData = JSON.parse(fs.readFileSync(tasksPath, 'utf8'));
|
||||
if (!tasksData || typeof tasksData !== 'object') {
|
||||
return DEFAULT_TAGS;
|
||||
}
|
||||
|
||||
// Check if it's tagged format (has tag-like keys with tasks arrays)
|
||||
const potentialTags = Object.keys(tasksData).filter((key) =>
|
||||
_isValidTaggedTask(tasksData[key])
|
||||
);
|
||||
|
||||
return potentialTags.length > 0 ? potentialTags : DEFAULT_TAGS;
|
||||
} catch (readError) {
|
||||
if (getDebugFlag()) {
|
||||
log(
|
||||
'debug',
|
||||
`Could not read tasks file for available tags: ${readError.message}`
|
||||
);
|
||||
}
|
||||
return DEFAULT_TAGS;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to validate tagged task structure
|
||||
function _isValidTaggedTask(taskData) {
|
||||
return (
|
||||
taskData && typeof taskData === 'object' && Array.isArray(taskData.tasks)
|
||||
);
|
||||
}
|
||||
|
||||
// --- Configuration for Retries ---
|
||||
const MAX_RETRIES = 2;
|
||||
const INITIAL_RETRY_DELAY_MS = 1000;
|
||||
@@ -244,6 +268,65 @@ function _extractErrorMessage(error) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get role configuration (provider and model) based on role type
|
||||
* @param {string} role - The role ('main', 'research', 'fallback')
|
||||
* @param {string} projectRoot - Project root path
|
||||
* @returns {Object|null} Configuration object with provider and modelId
|
||||
*/
|
||||
function _getRoleConfiguration(role, projectRoot) {
|
||||
const roleConfigs = {
|
||||
main: {
|
||||
provider: getMainProvider(projectRoot),
|
||||
modelId: getMainModelId(projectRoot)
|
||||
},
|
||||
research: {
|
||||
provider: getResearchProvider(projectRoot),
|
||||
modelId: getResearchModelId(projectRoot)
|
||||
},
|
||||
fallback: {
|
||||
provider: getFallbackProvider(projectRoot),
|
||||
modelId: getFallbackModelId(projectRoot)
|
||||
}
|
||||
};
|
||||
|
||||
return roleConfigs[role] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Vertex AI specific configuration
|
||||
* @param {string} projectRoot - Project root path
|
||||
* @param {Object} session - Session object
|
||||
* @returns {Object} Vertex AI configuration parameters
|
||||
*/
|
||||
function _getVertexConfiguration(projectRoot, session) {
|
||||
const projectId =
|
||||
getVertexProjectId(projectRoot) ||
|
||||
resolveEnvVariable('VERTEX_PROJECT_ID', session, projectRoot);
|
||||
|
||||
const location =
|
||||
getVertexLocation(projectRoot) ||
|
||||
resolveEnvVariable('VERTEX_LOCATION', session, projectRoot) ||
|
||||
'us-central1';
|
||||
|
||||
const credentialsPath = resolveEnvVariable(
|
||||
'GOOGLE_APPLICATION_CREDENTIALS',
|
||||
session,
|
||||
projectRoot
|
||||
);
|
||||
|
||||
log(
|
||||
'debug',
|
||||
`Using Vertex AI configuration: Project ID=${projectId}, Location=${location}`
|
||||
);
|
||||
|
||||
return {
|
||||
projectId,
|
||||
location,
|
||||
...(credentialsPath && { credentials: { credentialsFromEnv: true } })
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal helper to resolve the API key for a given provider.
|
||||
* @param {string} providerName - The name of the provider (lowercase).
|
||||
@@ -424,18 +507,13 @@ async function _unifiedServiceRunner(serviceType, params) {
|
||||
let telemetryData = null;
|
||||
|
||||
try {
|
||||
log('info', `New AI service call with role: ${currentRole}`);
|
||||
log('debug', `New AI service call with role: ${currentRole}`);
|
||||
|
||||
if (currentRole === 'main') {
|
||||
providerName = getMainProvider(effectiveProjectRoot);
|
||||
modelId = getMainModelId(effectiveProjectRoot);
|
||||
} else if (currentRole === 'research') {
|
||||
providerName = getResearchProvider(effectiveProjectRoot);
|
||||
modelId = getResearchModelId(effectiveProjectRoot);
|
||||
} else if (currentRole === 'fallback') {
|
||||
providerName = getFallbackProvider(effectiveProjectRoot);
|
||||
modelId = getFallbackModelId(effectiveProjectRoot);
|
||||
} else {
|
||||
const roleConfig = _getRoleConfiguration(
|
||||
currentRole,
|
||||
effectiveProjectRoot
|
||||
);
|
||||
if (!roleConfig) {
|
||||
log(
|
||||
'error',
|
||||
`Unknown role encountered in _unifiedServiceRunner: ${currentRole}`
|
||||
@@ -444,6 +522,8 @@ async function _unifiedServiceRunner(serviceType, params) {
|
||||
lastError || new Error(`Unknown AI role specified: ${currentRole}`);
|
||||
continue;
|
||||
}
|
||||
providerName = roleConfig.provider;
|
||||
modelId = roleConfig.modelId;
|
||||
|
||||
if (!providerName || !modelId) {
|
||||
log(
|
||||
@@ -517,41 +597,9 @@ async function _unifiedServiceRunner(serviceType, params) {
|
||||
|
||||
// Handle Vertex AI specific configuration
|
||||
if (providerName?.toLowerCase() === 'vertex') {
|
||||
// Get Vertex project ID and location
|
||||
const projectId =
|
||||
getVertexProjectId(effectiveProjectRoot) ||
|
||||
resolveEnvVariable(
|
||||
'VERTEX_PROJECT_ID',
|
||||
session,
|
||||
effectiveProjectRoot
|
||||
);
|
||||
|
||||
const location =
|
||||
getVertexLocation(effectiveProjectRoot) ||
|
||||
resolveEnvVariable(
|
||||
'VERTEX_LOCATION',
|
||||
session,
|
||||
effectiveProjectRoot
|
||||
) ||
|
||||
'us-central1';
|
||||
|
||||
// Get credentials path if available
|
||||
const credentialsPath = resolveEnvVariable(
|
||||
'GOOGLE_APPLICATION_CREDENTIALS',
|
||||
session,
|
||||
effectiveProjectRoot
|
||||
);
|
||||
|
||||
// Add Vertex-specific parameters
|
||||
providerSpecificParams = {
|
||||
projectId,
|
||||
location,
|
||||
...(credentialsPath && { credentials: { credentialsFromEnv: true } })
|
||||
};
|
||||
|
||||
log(
|
||||
'debug',
|
||||
`Using Vertex AI configuration: Project ID=${projectId}, Location=${location}`
|
||||
providerSpecificParams = _getVertexConfiguration(
|
||||
effectiveProjectRoot,
|
||||
session
|
||||
);
|
||||
}
|
||||
|
||||
@@ -594,7 +642,8 @@ async function _unifiedServiceRunner(serviceType, params) {
|
||||
temperature: roleParams.temperature,
|
||||
messages,
|
||||
...(baseURL && { baseURL }),
|
||||
...(serviceType === 'generateObject' && { schema, objectName }),
|
||||
...((serviceType === 'generateObject' ||
|
||||
serviceType === 'streamObject') && { schema, objectName }),
|
||||
...providerSpecificParams,
|
||||
...restApiParams
|
||||
};
|
||||
@@ -635,7 +684,10 @@ async function _unifiedServiceRunner(serviceType, params) {
|
||||
finalMainResult = providerResponse.text;
|
||||
} else if (serviceType === 'generateObject') {
|
||||
finalMainResult = providerResponse.object;
|
||||
} else if (serviceType === 'streamText') {
|
||||
} else if (
|
||||
serviceType === 'streamText' ||
|
||||
serviceType === 'streamObject'
|
||||
) {
|
||||
finalMainResult = providerResponse;
|
||||
} else {
|
||||
log(
|
||||
@@ -651,7 +703,9 @@ async function _unifiedServiceRunner(serviceType, params) {
|
||||
return {
|
||||
mainResult: finalMainResult,
|
||||
telemetryData: telemetryData,
|
||||
tagInfo: tagInfo
|
||||
tagInfo: tagInfo,
|
||||
providerName: providerName,
|
||||
modelId: modelId
|
||||
};
|
||||
} catch (error) {
|
||||
const cleanMessage = _extractErrorMessage(error);
|
||||
@@ -732,6 +786,31 @@ async function streamTextService(params) {
|
||||
return _unifiedServiceRunner('streamText', combinedParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unified service function for streaming structured objects.
|
||||
* Uses Vercel AI SDK's streamObject for proper JSON streaming.
|
||||
*
|
||||
* @param {object} params - Parameters for the service call.
|
||||
* @param {string} params.role - The initial client role ('main', 'research', 'fallback').
|
||||
* @param {object} [params.session=null] - Optional MCP session object.
|
||||
* @param {string} [params.projectRoot=null] - Optional project root path for .env fallback.
|
||||
* @param {import('zod').ZodSchema} params.schema - The Zod schema for the expected object.
|
||||
* @param {string} params.prompt - The prompt for the AI.
|
||||
* @param {string} [params.systemPrompt] - Optional system prompt.
|
||||
* @param {string} params.commandName - Name of the command invoking the service.
|
||||
* @param {string} [params.outputType='cli'] - 'cli' or 'mcp'.
|
||||
* @returns {Promise<object>} Result object containing the stream and usage data.
|
||||
*/
|
||||
async function streamObjectService(params) {
|
||||
const defaults = { outputType: 'cli' };
|
||||
const combinedParams = { ...defaults, ...params };
|
||||
// Stream object requires a schema
|
||||
if (!combinedParams.schema) {
|
||||
throw new Error('streamObjectService requires a schema parameter');
|
||||
}
|
||||
return _unifiedServiceRunner('streamObject', combinedParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unified service function for generating structured objects.
|
||||
* Handles client retrieval, retries, and fallback sequence.
|
||||
@@ -792,9 +871,12 @@ async function logAiUsage({
|
||||
modelId
|
||||
);
|
||||
|
||||
const totalCost =
|
||||
((inputTokens || 0) / 1_000_000) * inputCost +
|
||||
((outputTokens || 0) / 1_000_000) * outputCost;
|
||||
const totalCost = _calculateCost(
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
inputCost,
|
||||
outputCost
|
||||
);
|
||||
|
||||
const telemetryData = {
|
||||
timestamp,
|
||||
@@ -805,7 +887,7 @@ async function logAiUsage({
|
||||
inputTokens: inputTokens || 0,
|
||||
outputTokens: outputTokens || 0,
|
||||
totalTokens,
|
||||
totalCost: parseFloat(totalCost.toFixed(6)),
|
||||
totalCost,
|
||||
currency // Add currency to the telemetry data
|
||||
};
|
||||
|
||||
@@ -828,6 +910,7 @@ async function logAiUsage({
|
||||
export {
|
||||
generateTextService,
|
||||
streamTextService,
|
||||
streamObjectService,
|
||||
generateObjectService,
|
||||
logAiUsage
|
||||
};
|
||||
|
||||
@@ -912,8 +912,6 @@ function registerCommands(programInstance) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let spinner;
|
||||
|
||||
try {
|
||||
if (!(await confirmOverwriteIfNeeded())) return;
|
||||
|
||||
@@ -930,7 +928,6 @@ function registerCommands(programInstance) {
|
||||
);
|
||||
}
|
||||
|
||||
spinner = ora('Parsing PRD and generating tasks...\n').start();
|
||||
// Handle case where getTasksPath() returns null
|
||||
const outputPath =
|
||||
taskMaster.getTasksPath() ||
|
||||
@@ -942,13 +939,8 @@ function registerCommands(programInstance) {
|
||||
projectRoot: taskMaster.getProjectRoot(),
|
||||
tag: tag
|
||||
});
|
||||
spinner.succeed('Tasks generated successfully!');
|
||||
} catch (error) {
|
||||
if (spinner) {
|
||||
spinner.fail(`Error parsing PRD: ${error.message}`);
|
||||
} else {
|
||||
console.error(chalk.red(`Error parsing PRD: ${error.message}`));
|
||||
}
|
||||
console.error(chalk.red(`Error parsing PRD: ${error.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
@@ -4111,12 +4103,7 @@ Examples:
|
||||
' task-master move --from=5 --from-tag=backlog --to-tag=in-progress --ignore-dependencies'
|
||||
) +
|
||||
'\n\n' +
|
||||
chalk.white(' # Force move (may break dependencies)') +
|
||||
'\n' +
|
||||
chalk.white(
|
||||
' task-master move --from=5 --from-tag=backlog --to-tag=in-progress --force'
|
||||
) +
|
||||
'\n\n' +
|
||||
chalk.yellow.bold('Best Practices:') +
|
||||
'\n' +
|
||||
chalk.white(
|
||||
@@ -4127,10 +4114,6 @@ Examples:
|
||||
' • Use --ignore-dependencies to break cross-tag dependencies'
|
||||
) +
|
||||
'\n' +
|
||||
chalk.white(
|
||||
' • Use --force only when you understand the consequences'
|
||||
) +
|
||||
'\n' +
|
||||
chalk.white(
|
||||
' • Check dependencies first: task-master validate-dependencies'
|
||||
) +
|
||||
@@ -4192,6 +4175,12 @@ Examples:
|
||||
|
||||
console.log(chalk.green(`✓ ${result.message}`));
|
||||
|
||||
// Print any tips returned from the move operation (e.g., after ignoring dependencies)
|
||||
if (Array.isArray(result.tips) && result.tips.length > 0) {
|
||||
console.log('\n' + chalk.yellow.bold('Next Steps:'));
|
||||
result.tips.forEach((t) => console.log(chalk.white(` • ${t}`)));
|
||||
}
|
||||
|
||||
// Check if source tag still contains tasks before regenerating files
|
||||
const tasksData = readJSON(
|
||||
taskMaster.getTasksPath(),
|
||||
@@ -4458,6 +4447,27 @@ Examples:
|
||||
await handleWithinTagMove(moveContext);
|
||||
}
|
||||
} catch (error) {
|
||||
const errMsg = String(error && (error.message || error));
|
||||
if (errMsg.includes('already exists in target tag')) {
|
||||
console.error(chalk.red(`Error: ${errMsg}`));
|
||||
console.log(
|
||||
'\n' +
|
||||
chalk.yellow.bold('Conflict: ID already exists in target tag') +
|
||||
'\n' +
|
||||
chalk.white(
|
||||
' • Choose a different target tag without conflicting IDs'
|
||||
) +
|
||||
'\n' +
|
||||
chalk.white(
|
||||
' • Move a different set of IDs (avoid existing ones)'
|
||||
) +
|
||||
'\n' +
|
||||
chalk.white(
|
||||
' • If needed, move within-tag to a new ID first, then cross-tag move'
|
||||
)
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
handleMoveError(error, moveContext);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -72,7 +72,8 @@ const DEFAULTS = {
|
||||
projectName: 'Task Master',
|
||||
ollamaBaseURL: 'http://localhost:11434/api',
|
||||
bedrockBaseURL: 'https://bedrock.us-east-1.amazonaws.com',
|
||||
responseLanguage: 'English'
|
||||
responseLanguage: 'English',
|
||||
enableCodebaseAnalysis: true
|
||||
},
|
||||
claudeCode: {}
|
||||
};
|
||||
@@ -427,6 +428,63 @@ function getResearchProvider(explicitRoot = null) {
|
||||
return getModelConfigForRole('research', explicitRoot).provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if codebase analysis feature flag is enabled across all sources
|
||||
* Priority: .env > MCP env > config.json
|
||||
* @param {object|null} session - MCP session object (optional)
|
||||
* @param {string|null} projectRoot - Project root path (optional)
|
||||
* @returns {boolean} True if codebase analysis is enabled
|
||||
*/
|
||||
function isCodebaseAnalysisEnabled(session = null, projectRoot = null) {
|
||||
// Priority 1: Environment variable
|
||||
const envFlag = resolveEnvVariable(
|
||||
'TASKMASTER_ENABLE_CODEBASE_ANALYSIS',
|
||||
session,
|
||||
projectRoot
|
||||
);
|
||||
if (envFlag !== null && envFlag !== undefined && envFlag !== '') {
|
||||
return envFlag.toLowerCase() === 'true' || envFlag === '1';
|
||||
}
|
||||
|
||||
// Priority 2: MCP session environment
|
||||
if (session?.env?.TASKMASTER_ENABLE_CODEBASE_ANALYSIS) {
|
||||
const mcpFlag = session.env.TASKMASTER_ENABLE_CODEBASE_ANALYSIS;
|
||||
return mcpFlag.toLowerCase() === 'true' || mcpFlag === '1';
|
||||
}
|
||||
|
||||
// Priority 3: Configuration file
|
||||
const globalConfig = getGlobalConfig(projectRoot);
|
||||
return globalConfig.enableCodebaseAnalysis !== false; // Default to true
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if codebase analysis is available and enabled
|
||||
* @param {boolean} useResearch - Whether to check research provider or main provider
|
||||
* @param {string|null} projectRoot - Project root path (optional)
|
||||
* @param {object|null} session - MCP session object (optional)
|
||||
* @returns {boolean} True if codebase analysis is available and enabled
|
||||
*/
|
||||
function hasCodebaseAnalysis(
|
||||
useResearch = false,
|
||||
projectRoot = null,
|
||||
session = null
|
||||
) {
|
||||
// First check if the feature is enabled
|
||||
if (!isCodebaseAnalysisEnabled(session, projectRoot)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Then check if a codebase analysis provider is configured
|
||||
const currentProvider = useResearch
|
||||
? getResearchProvider(projectRoot)
|
||||
: getMainProvider(projectRoot);
|
||||
|
||||
return (
|
||||
currentProvider === CUSTOM_PROVIDERS.CLAUDE_CODE ||
|
||||
currentProvider === CUSTOM_PROVIDERS.GEMINI_CLI
|
||||
);
|
||||
}
|
||||
|
||||
function getResearchModelId(explicitRoot = null) {
|
||||
return getModelConfigForRole('research', explicitRoot).modelId;
|
||||
}
|
||||
@@ -542,6 +600,11 @@ function getResponseLanguage(explicitRoot = null) {
|
||||
return getGlobalConfig(explicitRoot).responseLanguage;
|
||||
}
|
||||
|
||||
function getCodebaseAnalysisEnabled(explicitRoot = null) {
|
||||
// Directly return value from config
|
||||
return getGlobalConfig(explicitRoot).enableCodebaseAnalysis;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets model parameters (maxTokens, temperature) for a specific role,
|
||||
* considering model-specific overrides from supported-models.json.
|
||||
@@ -983,6 +1046,7 @@ export {
|
||||
getResearchModelId,
|
||||
getResearchMaxTokens,
|
||||
getResearchTemperature,
|
||||
hasCodebaseAnalysis,
|
||||
getFallbackProvider,
|
||||
getFallbackModelId,
|
||||
getFallbackMaxTokens,
|
||||
@@ -999,6 +1063,8 @@ export {
|
||||
getAzureBaseURL,
|
||||
getBedrockBaseURL,
|
||||
getResponseLanguage,
|
||||
getCodebaseAnalysisEnabled,
|
||||
isCodebaseAnalysisEnabled,
|
||||
getParametersForRole,
|
||||
getUserId,
|
||||
// API Key Checkers (still relevant)
|
||||
|
||||
@@ -36,7 +36,7 @@ export class PromptManager {
|
||||
const schema = JSON.parse(schemaContent);
|
||||
|
||||
this.validatePrompt = this.ajv.compile(schema);
|
||||
log('info', '✓ JSON schema validation enabled');
|
||||
log('debug', '✓ JSON schema validation enabled');
|
||||
} catch (error) {
|
||||
log('warn', `⚠ Schema validation disabled: ${error.message}`);
|
||||
this.validatePrompt = () => true; // Fallback to no validation
|
||||
|
||||
@@ -786,6 +786,39 @@
|
||||
}
|
||||
],
|
||||
"ollama": [
|
||||
{
|
||||
"id": "gpt-oss:latest",
|
||||
"swe_score": 0.607,
|
||||
"cost_per_1m_tokens": {
|
||||
"input": 0,
|
||||
"output": 0
|
||||
},
|
||||
"allowed_roles": ["main", "fallback"],
|
||||
"max_tokens": 128000,
|
||||
"supported": true
|
||||
},
|
||||
{
|
||||
"id": "gpt-oss:20b",
|
||||
"swe_score": 0.607,
|
||||
"cost_per_1m_tokens": {
|
||||
"input": 0,
|
||||
"output": 0
|
||||
},
|
||||
"allowed_roles": ["main", "fallback"],
|
||||
"max_tokens": 128000,
|
||||
"supported": true
|
||||
},
|
||||
{
|
||||
"id": "gpt-oss:120b",
|
||||
"swe_score": 0.624,
|
||||
"cost_per_1m_tokens": {
|
||||
"input": 0,
|
||||
"output": 0
|
||||
},
|
||||
"allowed_roles": ["main", "fallback"],
|
||||
"max_tokens": 128000,
|
||||
"supported": true
|
||||
},
|
||||
{
|
||||
"id": "devstral:latest",
|
||||
"swe_score": 0,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { findTaskById } from './utils.js';
|
||||
import parsePRD from './task-manager/parse-prd.js';
|
||||
import parsePRD from './task-manager/parse-prd/index.js';
|
||||
import updateTasks from './task-manager/update-tasks.js';
|
||||
import updateTaskById from './task-manager/update-task-by-id.js';
|
||||
import generateTaskFiles from './task-manager/generate-task-files.js';
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
markMigrationForNotice
|
||||
} from '../utils.js';
|
||||
import { generateObjectService } from '../ai-services-unified.js';
|
||||
import { getDefaultPriority } from '../config-manager.js';
|
||||
import { getDefaultPriority, hasCodebaseAnalysis } from '../config-manager.js';
|
||||
import { getPromptManager } from '../prompt-manager.js';
|
||||
import ContextGatherer from '../utils/contextGatherer.js';
|
||||
import generateTaskFiles from './generate-task-files.js';
|
||||
@@ -425,7 +425,13 @@ async function addTask(
|
||||
contextFromArgs,
|
||||
useResearch,
|
||||
priority: effectivePriority,
|
||||
dependencies: numericDependencies
|
||||
dependencies: numericDependencies,
|
||||
hasCodebaseAnalysis: hasCodebaseAnalysis(
|
||||
useResearch,
|
||||
projectRoot,
|
||||
session
|
||||
),
|
||||
projectRoot: projectRoot
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import boxen from 'boxen';
|
||||
import readline from 'readline';
|
||||
import fs from 'fs';
|
||||
|
||||
import { log, readJSON, writeJSON, isSilentMode } from '../utils.js';
|
||||
import { log, readJSON, isSilentMode } from '../utils.js';
|
||||
|
||||
import {
|
||||
startLoadingIndicator,
|
||||
@@ -16,8 +16,7 @@ import { generateTextService } from '../ai-services-unified.js';
|
||||
import {
|
||||
getDebugFlag,
|
||||
getProjectName,
|
||||
getMainProvider,
|
||||
getResearchProvider
|
||||
hasCodebaseAnalysis
|
||||
} from '../config-manager.js';
|
||||
import { getPromptManager } from '../prompt-manager.js';
|
||||
import {
|
||||
@@ -415,16 +414,16 @@ async function analyzeTaskComplexity(options, context = {}) {
|
||||
const promptManager = getPromptManager();
|
||||
|
||||
// Check if Claude Code is being used as the provider
|
||||
const currentProvider = useResearch
|
||||
? getResearchProvider(projectRoot)
|
||||
: getMainProvider(projectRoot);
|
||||
const isClaudeCode = currentProvider === CUSTOM_PROVIDERS.CLAUDE_CODE;
|
||||
|
||||
const promptParams = {
|
||||
tasks: tasksData.tasks,
|
||||
gatheredContext: gatheredContext || '',
|
||||
useResearch: useResearch,
|
||||
isClaudeCode: isClaudeCode,
|
||||
hasCodebaseAnalysis: hasCodebaseAnalysis(
|
||||
useResearch,
|
||||
projectRoot,
|
||||
session
|
||||
),
|
||||
projectRoot: projectRoot || ''
|
||||
};
|
||||
|
||||
|
||||
@@ -21,13 +21,11 @@ import { generateTextService } from '../ai-services-unified.js';
|
||||
import {
|
||||
getDefaultSubtasks,
|
||||
getDebugFlag,
|
||||
getMainProvider,
|
||||
getResearchProvider
|
||||
hasCodebaseAnalysis
|
||||
} from '../config-manager.js';
|
||||
import { getPromptManager } from '../prompt-manager.js';
|
||||
import generateTaskFiles from './generate-task-files.js';
|
||||
import { COMPLEXITY_REPORT_FILE } from '../../../src/constants/paths.js';
|
||||
import { CUSTOM_PROVIDERS } from '../../../src/constants/providers.js';
|
||||
import { ContextGatherer } from '../utils/contextGatherer.js';
|
||||
import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';
|
||||
import { flattenTasksWithSubtasks, findProjectRoot } from '../utils.js';
|
||||
@@ -457,11 +455,12 @@ async function expandTask(
|
||||
// Load prompts using PromptManager
|
||||
const promptManager = getPromptManager();
|
||||
|
||||
// Check if Claude Code is being used as the provider
|
||||
const currentProvider = useResearch
|
||||
? getResearchProvider(projectRoot)
|
||||
: getMainProvider(projectRoot);
|
||||
const isClaudeCode = currentProvider === CUSTOM_PROVIDERS.CLAUDE_CODE;
|
||||
// Check if a codebase analysis provider is being used
|
||||
const hasCodebaseAnalysisCapability = hasCodebaseAnalysis(
|
||||
useResearch,
|
||||
projectRoot,
|
||||
session
|
||||
);
|
||||
|
||||
// Combine all context sources into a single additionalContext parameter
|
||||
let combinedAdditionalContext = '';
|
||||
@@ -508,7 +507,7 @@ async function expandTask(
|
||||
gatheredContext: gatheredContextText || '',
|
||||
useResearch: useResearch,
|
||||
expansionPrompt: expansionPromptText || undefined,
|
||||
isClaudeCode: isClaudeCode,
|
||||
hasCodebaseAnalysis: hasCodebaseAnalysisCapability,
|
||||
projectRoot: projectRoot || ''
|
||||
};
|
||||
|
||||
|
||||
@@ -61,6 +61,43 @@ const MOVE_ERROR_CODES = {
|
||||
INVALID_TARGET_TAG: 'INVALID_TARGET_TAG'
|
||||
};
|
||||
|
||||
/**
|
||||
* Normalize a dependency value to its numeric parent task ID.
|
||||
* - Numbers are returned as-is (if finite)
|
||||
* - Numeric strings are parsed ("5" -> 5)
|
||||
* - Dotted strings return the parent portion ("5.2" -> 5)
|
||||
* - Empty/invalid values return null
|
||||
* - null/undefined are preserved
|
||||
* @param {number|string|null|undefined} dep
|
||||
* @returns {number|null|undefined}
|
||||
*/
|
||||
function normalizeDependency(dep) {
|
||||
if (dep === null || dep === undefined) return dep;
|
||||
if (typeof dep === 'number') return Number.isFinite(dep) ? dep : null;
|
||||
if (typeof dep === 'string') {
|
||||
const trimmed = dep.trim();
|
||||
if (trimmed === '') return null;
|
||||
const parentPart = trimmed.includes('.') ? trimmed.split('.')[0] : trimmed;
|
||||
const parsed = parseInt(parentPart, 10);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize an array of dependency values to numeric IDs.
|
||||
* Preserves null/undefined input (returns as-is) and filters out invalid entries.
|
||||
* @param {Array<any>|null|undefined} deps
|
||||
* @returns {Array<number>|null|undefined}
|
||||
*/
|
||||
function normalizeDependencies(deps) {
|
||||
if (deps === null || deps === undefined) return deps;
|
||||
if (!Array.isArray(deps)) return deps;
|
||||
return deps
|
||||
.map((d) => normalizeDependency(d))
|
||||
.filter((n) => Number.isFinite(n));
|
||||
}
|
||||
|
||||
/**
|
||||
* Move one or more tasks/subtasks to new positions
|
||||
* @param {string} tasksPath - Path to tasks.json file
|
||||
@@ -674,15 +711,34 @@ async function resolveDependencies(
|
||||
) {
|
||||
const { withDependencies = false, ignoreDependencies = false } = options;
|
||||
|
||||
// Scope allTasks to the source tag to avoid cross-tag contamination when
|
||||
// computing dependency chains for --with-dependencies
|
||||
const tasksInSourceTag = Array.isArray(allTasks)
|
||||
? allTasks.filter((t) => t && t.tag === sourceTag)
|
||||
: [];
|
||||
|
||||
// Handle --with-dependencies flag first (regardless of cross-tag dependencies)
|
||||
if (withDependencies) {
|
||||
// Move dependent tasks along with main tasks
|
||||
// Find ALL dependencies recursively within the same tag
|
||||
const allDependentTaskIds = findAllDependenciesRecursively(
|
||||
// Find ALL dependencies recursively, but only using tasks from the source tag
|
||||
const allDependentTaskIdsRaw = findAllDependenciesRecursively(
|
||||
sourceTasks,
|
||||
allTasks,
|
||||
tasksInSourceTag,
|
||||
{ maxDepth: 100, includeSelf: false }
|
||||
);
|
||||
|
||||
// Filter dependent IDs to those that actually exist in the source tag
|
||||
const sourceTagIds = new Set(
|
||||
tasksInSourceTag.map((t) =>
|
||||
typeof t.id === 'string' ? parseInt(t.id, 10) : t.id
|
||||
)
|
||||
);
|
||||
const allDependentTaskIds = allDependentTaskIdsRaw.filter((depId) => {
|
||||
// Only numeric task IDs are eligible to be moved (subtasks cannot be moved cross-tag)
|
||||
const normalizedId = normalizeDependency(depId);
|
||||
return Number.isFinite(normalizedId) && sourceTagIds.has(normalizedId);
|
||||
});
|
||||
|
||||
const allTaskIdsToMove = [...new Set([...taskIds, ...allDependentTaskIds])];
|
||||
|
||||
log(
|
||||
@@ -711,22 +767,31 @@ async function resolveDependencies(
|
||||
if (ignoreDependencies) {
|
||||
// Break cross-tag dependencies (edge case - shouldn't normally happen)
|
||||
sourceTasks.forEach((task) => {
|
||||
const sourceTagTasks = tasksInSourceTag;
|
||||
const targetTagTasks = Array.isArray(allTasks)
|
||||
? allTasks.filter((t) => t && t.tag === targetTag)
|
||||
: [];
|
||||
task.dependencies = task.dependencies.filter((depId) => {
|
||||
// Handle both task IDs and subtask IDs (e.g., "1.2")
|
||||
let depTask = null;
|
||||
if (typeof depId === 'string' && depId.includes('.')) {
|
||||
// It's a subtask ID - extract parent task ID and find the parent task
|
||||
const [parentId, subtaskId] = depId
|
||||
.split('.')
|
||||
.map((id) => parseInt(id, 10));
|
||||
depTask = allTasks.find((t) => t.id === parentId);
|
||||
} else {
|
||||
// It's a regular task ID - normalize to number for comparison
|
||||
const normalizedDepId =
|
||||
typeof depId === 'string' ? parseInt(depId, 10) : depId;
|
||||
depTask = allTasks.find((t) => t.id === normalizedDepId);
|
||||
const parentTaskId = normalizeDependency(depId);
|
||||
|
||||
// If dependency resolves to a task in the source tag, drop it (would be cross-tag after move)
|
||||
if (
|
||||
Number.isFinite(parentTaskId) &&
|
||||
sourceTagTasks.some((t) => t.id === parentTaskId)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return !depTask || depTask.tag === targetTag;
|
||||
|
||||
// If dependency resolves to a task in the target tag, keep it
|
||||
if (
|
||||
Number.isFinite(parentTaskId) &&
|
||||
targetTagTasks.some((t) => t.id === parentTaskId)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Otherwise, keep as-is (unknown/unresolved dependency)
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -810,7 +875,16 @@ async function executeMoveOperation(
|
||||
if (existingTaskIndex !== -1) {
|
||||
throw new MoveTaskError(
|
||||
MOVE_ERROR_CODES.TASK_ALREADY_EXISTS,
|
||||
`Task ${taskId} already exists in target tag "${targetTag}"`
|
||||
`Task ${taskId} already exists in target tag "${targetTag}"`,
|
||||
{
|
||||
conflictingId: normalizedTaskId,
|
||||
targetTag,
|
||||
suggestions: [
|
||||
'Choose a different target tag without conflicting IDs',
|
||||
'Move a different set of IDs (avoid existing ones)',
|
||||
'If needed, move within-tag to a new ID first, then cross-tag move'
|
||||
]
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -851,7 +925,8 @@ async function finalizeMove(
|
||||
tasksPath,
|
||||
context,
|
||||
sourceTag,
|
||||
targetTag
|
||||
targetTag,
|
||||
dependencyResolution
|
||||
) {
|
||||
const { projectRoot } = context;
|
||||
const { rawData, movedTasks } = moveResult;
|
||||
@@ -859,10 +934,23 @@ async function finalizeMove(
|
||||
// Write the updated data
|
||||
writeJSON(tasksPath, rawData, projectRoot, null);
|
||||
|
||||
return {
|
||||
const response = {
|
||||
message: `Successfully moved ${movedTasks.length} tasks from "${sourceTag}" to "${targetTag}"`,
|
||||
movedTasks
|
||||
};
|
||||
|
||||
// If we intentionally broke cross-tag dependencies, provide tips to validate & fix
|
||||
if (
|
||||
dependencyResolution &&
|
||||
dependencyResolution.type === 'ignored-dependencies'
|
||||
) {
|
||||
response.tips = [
|
||||
'Run "task-master validate-dependencies" to check for dependency issues.',
|
||||
'Run "task-master fix-dependencies" to automatically repair dangling dependencies.'
|
||||
];
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -898,7 +986,7 @@ async function moveTasksBetweenTags(
|
||||
const { rawData, sourceTasks, allTasks } = await prepareTaskData(validation);
|
||||
|
||||
// 3. Handle dependencies
|
||||
const { tasksToMove } = await resolveDependencies(
|
||||
const { tasksToMove, dependencyResolution } = await resolveDependencies(
|
||||
sourceTasks,
|
||||
allTasks,
|
||||
options,
|
||||
@@ -923,7 +1011,8 @@ async function moveTasksBetweenTags(
|
||||
tasksPath,
|
||||
context,
|
||||
sourceTag,
|
||||
targetTag
|
||||
targetTag,
|
||||
dependencyResolution
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,395 +0,0 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import chalk from 'chalk';
|
||||
import boxen from 'boxen';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
log,
|
||||
writeJSON,
|
||||
enableSilentMode,
|
||||
disableSilentMode,
|
||||
isSilentMode,
|
||||
readJSON,
|
||||
findTaskById,
|
||||
ensureTagMetadata,
|
||||
getCurrentTag
|
||||
} from '../utils.js';
|
||||
|
||||
import { generateObjectService } from '../ai-services-unified.js';
|
||||
import {
|
||||
getDebugFlag,
|
||||
getMainProvider,
|
||||
getResearchProvider,
|
||||
getDefaultPriority
|
||||
} from '../config-manager.js';
|
||||
import { getPromptManager } from '../prompt-manager.js';
|
||||
import { displayAiUsageSummary } from '../ui.js';
|
||||
import { CUSTOM_PROVIDERS } from '../../../src/constants/providers.js';
|
||||
|
||||
// Define the Zod schema for a SINGLE task object
|
||||
const prdSingleTaskSchema = z.object({
|
||||
id: z.number(),
|
||||
title: z.string().min(1),
|
||||
description: z.string().min(1),
|
||||
details: z.string(),
|
||||
testStrategy: z.string(),
|
||||
priority: z.enum(['high', 'medium', 'low']),
|
||||
dependencies: z.array(z.number()),
|
||||
status: z.string()
|
||||
});
|
||||
|
||||
// Define the Zod schema for the ENTIRE expected AI response object
|
||||
const prdResponseSchema = z.object({
|
||||
tasks: z.array(prdSingleTaskSchema),
|
||||
metadata: z.object({
|
||||
projectName: z.string(),
|
||||
totalTasks: z.number(),
|
||||
sourceFile: z.string(),
|
||||
generatedAt: z.string()
|
||||
})
|
||||
});
|
||||
|
||||
/**
|
||||
* Parse a PRD file and generate tasks
|
||||
* @param {string} prdPath - Path to the PRD file
|
||||
* @param {string} tasksPath - Path to the tasks.json file
|
||||
* @param {number} numTasks - Number of tasks to generate
|
||||
* @param {Object} options - Additional options
|
||||
* @param {boolean} [options.force=false] - Whether to overwrite existing tasks.json.
|
||||
* @param {boolean} [options.append=false] - Append to existing tasks file.
|
||||
* @param {boolean} [options.research=false] - Use research model for enhanced PRD analysis.
|
||||
* @param {Object} [options.reportProgress] - Function to report progress (optional, likely unused).
|
||||
* @param {Object} [options.mcpLog] - MCP logger object (optional).
|
||||
* @param {Object} [options.session] - Session object from MCP server (optional).
|
||||
* @param {string} [options.projectRoot] - Project root path (for MCP/env fallback).
|
||||
* @param {string} [options.tag] - Target tag for task generation.
|
||||
* @param {string} [outputFormat='text'] - Output format ('text' or 'json').
|
||||
*/
|
||||
async function parsePRD(prdPath, tasksPath, numTasks, options = {}) {
|
||||
const {
|
||||
reportProgress,
|
||||
mcpLog,
|
||||
session,
|
||||
projectRoot,
|
||||
force = false,
|
||||
append = false,
|
||||
research = false,
|
||||
tag
|
||||
} = options;
|
||||
const isMCP = !!mcpLog;
|
||||
const outputFormat = isMCP ? 'json' : 'text';
|
||||
|
||||
// Use the provided tag, or the current active tag, or default to 'master'
|
||||
const targetTag = tag;
|
||||
|
||||
const logFn = mcpLog
|
||||
? mcpLog
|
||||
: {
|
||||
// Wrapper for CLI
|
||||
info: (...args) => log('info', ...args),
|
||||
warn: (...args) => log('warn', ...args),
|
||||
error: (...args) => log('error', ...args),
|
||||
debug: (...args) => log('debug', ...args),
|
||||
success: (...args) => log('success', ...args)
|
||||
};
|
||||
|
||||
// Create custom reporter using logFn
|
||||
const report = (message, level = 'info') => {
|
||||
// Check logFn directly
|
||||
if (logFn && typeof logFn[level] === 'function') {
|
||||
logFn[level](message);
|
||||
} else if (!isSilentMode() && outputFormat === 'text') {
|
||||
// Fallback to original log only if necessary and in CLI text mode
|
||||
log(level, message);
|
||||
}
|
||||
};
|
||||
|
||||
report(
|
||||
`Parsing PRD file: ${prdPath}, Force: ${force}, Append: ${append}, Research: ${research}`
|
||||
);
|
||||
|
||||
let existingTasks = [];
|
||||
let nextId = 1;
|
||||
let aiServiceResponse = null;
|
||||
|
||||
try {
|
||||
// Check if there are existing tasks in the target tag
|
||||
let hasExistingTasksInTag = false;
|
||||
if (fs.existsSync(tasksPath)) {
|
||||
try {
|
||||
// Read the entire file to check if the tag exists
|
||||
const existingFileContent = fs.readFileSync(tasksPath, 'utf8');
|
||||
const allData = JSON.parse(existingFileContent);
|
||||
|
||||
// Check if the target tag exists and has tasks
|
||||
if (
|
||||
allData[targetTag] &&
|
||||
Array.isArray(allData[targetTag].tasks) &&
|
||||
allData[targetTag].tasks.length > 0
|
||||
) {
|
||||
hasExistingTasksInTag = true;
|
||||
existingTasks = allData[targetTag].tasks;
|
||||
nextId = Math.max(...existingTasks.map((t) => t.id || 0)) + 1;
|
||||
}
|
||||
} catch (error) {
|
||||
// If we can't read the file or parse it, assume no existing tasks in this tag
|
||||
hasExistingTasksInTag = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle file existence and overwrite/append logic based on target tag
|
||||
if (hasExistingTasksInTag) {
|
||||
if (append) {
|
||||
report(
|
||||
`Append mode enabled. Found ${existingTasks.length} existing tasks in tag '${targetTag}'. Next ID will be ${nextId}.`,
|
||||
'info'
|
||||
);
|
||||
} else if (!force) {
|
||||
// Not appending and not forcing overwrite, and there are existing tasks in the target tag
|
||||
const overwriteError = new Error(
|
||||
`Tag '${targetTag}' already contains ${existingTasks.length} tasks. Use --force to overwrite or --append to add to existing tasks.`
|
||||
);
|
||||
report(overwriteError.message, 'error');
|
||||
if (outputFormat === 'text') {
|
||||
console.error(chalk.red(overwriteError.message));
|
||||
}
|
||||
throw overwriteError;
|
||||
} else {
|
||||
// Force overwrite is true
|
||||
report(
|
||||
`Force flag enabled. Overwriting existing tasks in tag '${targetTag}'.`,
|
||||
'info'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// No existing tasks in target tag, proceed without confirmation
|
||||
report(
|
||||
`Tag '${targetTag}' is empty or doesn't exist. Creating/updating tag with new tasks.`,
|
||||
'info'
|
||||
);
|
||||
}
|
||||
|
||||
report(`Reading PRD content from ${prdPath}`, 'info');
|
||||
const prdContent = fs.readFileSync(prdPath, 'utf8');
|
||||
if (!prdContent) {
|
||||
throw new Error(`Input file ${prdPath} is empty or could not be read.`);
|
||||
}
|
||||
|
||||
// Load prompts using PromptManager
|
||||
const promptManager = getPromptManager();
|
||||
|
||||
// Get defaultTaskPriority from config
|
||||
const defaultTaskPriority = getDefaultPriority(projectRoot) || 'medium';
|
||||
|
||||
// Check if Claude Code is being used as the provider
|
||||
const currentProvider = research
|
||||
? getResearchProvider(projectRoot)
|
||||
: getMainProvider(projectRoot);
|
||||
const isClaudeCode = currentProvider === CUSTOM_PROVIDERS.CLAUDE_CODE;
|
||||
|
||||
const { systemPrompt, userPrompt } = await promptManager.loadPrompt(
|
||||
'parse-prd',
|
||||
{
|
||||
research,
|
||||
numTasks,
|
||||
nextId,
|
||||
prdContent,
|
||||
prdPath,
|
||||
defaultTaskPriority,
|
||||
isClaudeCode,
|
||||
projectRoot: projectRoot || ''
|
||||
}
|
||||
);
|
||||
|
||||
// Call the unified AI service
|
||||
report(
|
||||
`Calling AI service to generate tasks from PRD${research ? ' with research-backed analysis' : ''}...`,
|
||||
'info'
|
||||
);
|
||||
|
||||
// Call generateObjectService with the CORRECT schema and additional telemetry params
|
||||
aiServiceResponse = await generateObjectService({
|
||||
role: research ? 'research' : 'main', // Use research role if flag is set
|
||||
session: session,
|
||||
projectRoot: projectRoot,
|
||||
schema: prdResponseSchema,
|
||||
objectName: 'tasks_data',
|
||||
systemPrompt: systemPrompt,
|
||||
prompt: userPrompt,
|
||||
commandName: 'parse-prd',
|
||||
outputType: isMCP ? 'mcp' : 'cli'
|
||||
});
|
||||
|
||||
// Create the directory if it doesn't exist
|
||||
const tasksDir = path.dirname(tasksPath);
|
||||
if (!fs.existsSync(tasksDir)) {
|
||||
fs.mkdirSync(tasksDir, { recursive: true });
|
||||
}
|
||||
logFn.success(
|
||||
`Successfully parsed PRD via AI service${research ? ' with research-backed analysis' : ''}.`
|
||||
);
|
||||
|
||||
// Validate and Process Tasks
|
||||
// const generatedData = aiServiceResponse?.mainResult?.object;
|
||||
|
||||
// Robustly get the actual AI-generated object
|
||||
let generatedData = null;
|
||||
if (aiServiceResponse?.mainResult) {
|
||||
if (
|
||||
typeof aiServiceResponse.mainResult === 'object' &&
|
||||
aiServiceResponse.mainResult !== null &&
|
||||
'tasks' in aiServiceResponse.mainResult
|
||||
) {
|
||||
// If mainResult itself is the object with a 'tasks' property
|
||||
generatedData = aiServiceResponse.mainResult;
|
||||
} else if (
|
||||
typeof aiServiceResponse.mainResult.object === 'object' &&
|
||||
aiServiceResponse.mainResult.object !== null &&
|
||||
'tasks' in aiServiceResponse.mainResult.object
|
||||
) {
|
||||
// If mainResult.object is the object with a 'tasks' property
|
||||
generatedData = aiServiceResponse.mainResult.object;
|
||||
}
|
||||
}
|
||||
|
||||
if (!generatedData || !Array.isArray(generatedData.tasks)) {
|
||||
logFn.error(
|
||||
`Internal Error: generateObjectService returned unexpected data structure: ${JSON.stringify(generatedData)}`
|
||||
);
|
||||
throw new Error(
|
||||
'AI service returned unexpected data structure after validation.'
|
||||
);
|
||||
}
|
||||
|
||||
let currentId = nextId;
|
||||
const taskMap = new Map();
|
||||
const processedNewTasks = generatedData.tasks.map((task) => {
|
||||
const newId = currentId++;
|
||||
taskMap.set(task.id, newId);
|
||||
return {
|
||||
...task,
|
||||
id: newId,
|
||||
status: task.status || 'pending',
|
||||
priority: task.priority || 'medium',
|
||||
dependencies: Array.isArray(task.dependencies) ? task.dependencies : [],
|
||||
subtasks: [],
|
||||
// Ensure all required fields have values (even if empty strings)
|
||||
title: task.title || '',
|
||||
description: task.description || '',
|
||||
details: task.details || '',
|
||||
testStrategy: task.testStrategy || ''
|
||||
};
|
||||
});
|
||||
|
||||
// Remap dependencies for the NEWLY processed tasks
|
||||
processedNewTasks.forEach((task) => {
|
||||
task.dependencies = task.dependencies
|
||||
.map((depId) => taskMap.get(depId)) // Map old AI ID to new sequential ID
|
||||
.filter(
|
||||
(newDepId) =>
|
||||
newDepId != null && // Must exist
|
||||
newDepId < task.id && // Must be a lower ID (could be existing or newly generated)
|
||||
(findTaskById(existingTasks, newDepId) || // Check if it exists in old tasks OR
|
||||
processedNewTasks.some((t) => t.id === newDepId)) // check if it exists in new tasks
|
||||
);
|
||||
});
|
||||
|
||||
const finalTasks = append
|
||||
? [...existingTasks, ...processedNewTasks]
|
||||
: processedNewTasks;
|
||||
|
||||
// Read the existing file to preserve other tags
|
||||
let outputData = {};
|
||||
if (fs.existsSync(tasksPath)) {
|
||||
try {
|
||||
const existingFileContent = fs.readFileSync(tasksPath, 'utf8');
|
||||
outputData = JSON.parse(existingFileContent);
|
||||
} catch (error) {
|
||||
// If we can't read the existing file, start with empty object
|
||||
outputData = {};
|
||||
}
|
||||
}
|
||||
|
||||
// Update only the target tag, preserving other tags
|
||||
outputData[targetTag] = {
|
||||
tasks: finalTasks,
|
||||
metadata: {
|
||||
created:
|
||||
outputData[targetTag]?.metadata?.created || new Date().toISOString(),
|
||||
updated: new Date().toISOString(),
|
||||
description: `Tasks for ${targetTag} context`
|
||||
}
|
||||
};
|
||||
|
||||
// Ensure the target tag has proper metadata
|
||||
ensureTagMetadata(outputData[targetTag], {
|
||||
description: `Tasks for ${targetTag} context`
|
||||
});
|
||||
|
||||
// Write the complete data structure back to the file
|
||||
fs.writeFileSync(tasksPath, JSON.stringify(outputData, null, 2));
|
||||
report(
|
||||
`Successfully ${append ? 'appended' : 'generated'} ${processedNewTasks.length} tasks in ${tasksPath}${research ? ' with research-backed analysis' : ''}`,
|
||||
'success'
|
||||
);
|
||||
|
||||
// Generate markdown task files after writing tasks.json
|
||||
// await generateTaskFiles(tasksPath, path.dirname(tasksPath), { mcpLog });
|
||||
|
||||
// Handle CLI output (e.g., success message)
|
||||
if (outputFormat === 'text') {
|
||||
console.log(
|
||||
boxen(
|
||||
chalk.green(
|
||||
`Successfully generated ${processedNewTasks.length} new tasks${research ? ' with research-backed analysis' : ''}. Total tasks in ${tasksPath}: ${finalTasks.length}`
|
||||
),
|
||||
{ padding: 1, borderColor: 'green', borderStyle: 'round' }
|
||||
)
|
||||
);
|
||||
|
||||
console.log(
|
||||
boxen(
|
||||
chalk.white.bold('Next Steps:') +
|
||||
'\n\n' +
|
||||
`${chalk.cyan('1.')} Run ${chalk.yellow('task-master list')} to view all tasks\n` +
|
||||
`${chalk.cyan('2.')} Run ${chalk.yellow('task-master expand --id=<id>')} to break down a task into subtasks`,
|
||||
{
|
||||
padding: 1,
|
||||
borderColor: 'cyan',
|
||||
borderStyle: 'round',
|
||||
margin: { top: 1 }
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
if (aiServiceResponse && aiServiceResponse.telemetryData) {
|
||||
displayAiUsageSummary(aiServiceResponse.telemetryData, 'cli');
|
||||
}
|
||||
}
|
||||
|
||||
// Return telemetry data
|
||||
return {
|
||||
success: true,
|
||||
tasksPath,
|
||||
telemetryData: aiServiceResponse?.telemetryData,
|
||||
tagInfo: aiServiceResponse?.tagInfo
|
||||
};
|
||||
} catch (error) {
|
||||
report(`Error parsing PRD: ${error.message}`, 'error');
|
||||
|
||||
// Only show error UI for text output (CLI)
|
||||
if (outputFormat === 'text') {
|
||||
console.error(chalk.red(`Error: ${error.message}`));
|
||||
|
||||
if (getDebugFlag(projectRoot)) {
|
||||
// Use projectRoot for debug flag check
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
throw error; // Always re-throw for proper error handling
|
||||
}
|
||||
}
|
||||
|
||||
export default parsePRD;
|
||||
3
scripts/modules/task-manager/parse-prd/index.js
Normal file
3
scripts/modules/task-manager/parse-prd/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
// Main entry point for parse-prd module
|
||||
export { default } from './parse-prd.js';
|
||||
export { default as parsePRD } from './parse-prd.js';
|
||||
108
scripts/modules/task-manager/parse-prd/parse-prd-config.js
Normal file
108
scripts/modules/task-manager/parse-prd/parse-prd-config.js
Normal file
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* Configuration classes and schemas for PRD parsing
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import { TASK_PRIORITY_OPTIONS } from '../../../../src/constants/task-priority.js';
|
||||
import { getCurrentTag, isSilentMode, log } from '../../utils.js';
|
||||
import { Duration } from '../../../../src/utils/timeout-manager.js';
|
||||
import { hasCodebaseAnalysis } from '../../config-manager.js';
|
||||
|
||||
// ============================================================================
|
||||
// SCHEMAS
|
||||
// ============================================================================
|
||||
|
||||
// Define the Zod schema for a SINGLE task object
|
||||
export const prdSingleTaskSchema = z.object({
|
||||
id: z.number(),
|
||||
title: z.string().min(1),
|
||||
description: z.string().min(1),
|
||||
details: z.string(),
|
||||
testStrategy: z.string(),
|
||||
priority: z.enum(TASK_PRIORITY_OPTIONS),
|
||||
dependencies: z.array(z.number()),
|
||||
status: z.string()
|
||||
});
|
||||
|
||||
// Define the Zod schema for the ENTIRE expected AI response object
|
||||
export const prdResponseSchema = z.object({
|
||||
tasks: z.array(prdSingleTaskSchema),
|
||||
metadata: z.object({
|
||||
projectName: z.string(),
|
||||
totalTasks: z.number(),
|
||||
sourceFile: z.string(),
|
||||
generatedAt: z.string()
|
||||
})
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// CONFIGURATION CLASSES
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Configuration object for PRD parsing
|
||||
*/
|
||||
export class PrdParseConfig {
|
||||
constructor(prdPath, tasksPath, numTasks, options = {}) {
|
||||
this.prdPath = prdPath;
|
||||
this.tasksPath = tasksPath;
|
||||
this.numTasks = numTasks;
|
||||
this.force = options.force || false;
|
||||
this.append = options.append || false;
|
||||
this.research = options.research || false;
|
||||
this.reportProgress = options.reportProgress;
|
||||
this.mcpLog = options.mcpLog;
|
||||
this.session = options.session;
|
||||
this.projectRoot = options.projectRoot;
|
||||
this.tag = options.tag;
|
||||
this.streamingTimeout =
|
||||
options.streamingTimeout || Duration.seconds(180).milliseconds;
|
||||
|
||||
// Derived values
|
||||
this.targetTag = this.tag || getCurrentTag(this.projectRoot) || 'master';
|
||||
this.isMCP = !!this.mcpLog;
|
||||
this.outputFormat = this.isMCP && !this.reportProgress ? 'json' : 'text';
|
||||
|
||||
// Feature flag: Temporarily disable streaming, use generateObject instead
|
||||
// TODO: Re-enable streaming once issues are resolved
|
||||
const ENABLE_STREAMING = false;
|
||||
|
||||
this.useStreaming =
|
||||
ENABLE_STREAMING &&
|
||||
(typeof this.reportProgress === 'function' ||
|
||||
this.outputFormat === 'text');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if codebase analysis is available (Claude Code or Gemini CLI)
|
||||
*/
|
||||
hasCodebaseAnalysis() {
|
||||
return hasCodebaseAnalysis(this.research, this.projectRoot, this.session);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logging configuration and utilities
|
||||
*/
|
||||
export class LoggingConfig {
|
||||
constructor(mcpLog, reportProgress) {
|
||||
this.isMCP = !!mcpLog;
|
||||
this.outputFormat = this.isMCP && !reportProgress ? 'json' : 'text';
|
||||
|
||||
this.logFn = mcpLog || {
|
||||
info: (...args) => log('info', ...args),
|
||||
warn: (...args) => log('warn', ...args),
|
||||
error: (...args) => log('error', ...args),
|
||||
debug: (...args) => log('debug', ...args),
|
||||
success: (...args) => log('success', ...args)
|
||||
};
|
||||
}
|
||||
|
||||
report(message, level = 'info') {
|
||||
if (this.logFn && typeof this.logFn[level] === 'function') {
|
||||
this.logFn[level](message);
|
||||
} else if (!isSilentMode() && this.outputFormat === 'text') {
|
||||
log(level, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
383
scripts/modules/task-manager/parse-prd/parse-prd-helpers.js
Normal file
383
scripts/modules/task-manager/parse-prd/parse-prd-helpers.js
Normal file
@@ -0,0 +1,383 @@
|
||||
/**
|
||||
* Helper functions for PRD parsing
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import boxen from 'boxen';
|
||||
import chalk from 'chalk';
|
||||
import { ensureTagMetadata, findTaskById } from '../../utils.js';
|
||||
import { displayParsePrdSummary } from '../../../../src/ui/parse-prd.js';
|
||||
import { TimeoutManager } from '../../../../src/utils/timeout-manager.js';
|
||||
import { displayAiUsageSummary } from '../../ui.js';
|
||||
import { getPromptManager } from '../../prompt-manager.js';
|
||||
import { getDefaultPriority } from '../../config-manager.js';
|
||||
|
||||
/**
|
||||
* Estimate token count from text
|
||||
* @param {string} text - Text to estimate tokens for
|
||||
* @returns {number} Estimated token count
|
||||
*/
|
||||
export function estimateTokens(text) {
|
||||
// Common approximation: ~4 characters per token for English
|
||||
return Math.ceil(text.length / 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and validate PRD content
|
||||
* @param {string} prdPath - Path to PRD file
|
||||
* @returns {string} PRD content
|
||||
* @throws {Error} If file is empty or cannot be read
|
||||
*/
|
||||
export function readPrdContent(prdPath) {
|
||||
const prdContent = fs.readFileSync(prdPath, 'utf8');
|
||||
if (!prdContent) {
|
||||
throw new Error(`Input file ${prdPath} is empty or could not be read.`);
|
||||
}
|
||||
return prdContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load existing tasks from file
|
||||
* @param {string} tasksPath - Path to tasks file
|
||||
* @param {string} targetTag - Target tag to load from
|
||||
* @returns {{tasks: Array, nextId: number}} Existing tasks and next ID
|
||||
*/
|
||||
export function loadExistingTasks(tasksPath, targetTag) {
|
||||
let existingTasks = [];
|
||||
let nextId = 1;
|
||||
|
||||
if (!fs.existsSync(tasksPath)) {
|
||||
return { existingTasks, nextId };
|
||||
}
|
||||
|
||||
try {
|
||||
const existingFileContent = fs.readFileSync(tasksPath, 'utf8');
|
||||
const allData = JSON.parse(existingFileContent);
|
||||
|
||||
if (allData[targetTag]?.tasks && Array.isArray(allData[targetTag].tasks)) {
|
||||
existingTasks = allData[targetTag].tasks;
|
||||
if (existingTasks.length > 0) {
|
||||
nextId = Math.max(...existingTasks.map((t) => t.id || 0)) + 1;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// If we can't read the file or parse it, assume no existing tasks
|
||||
return { existingTasks: [], nextId: 1 };
|
||||
}
|
||||
|
||||
return { existingTasks, nextId };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate overwrite/append operations
|
||||
* @param {Object} params
|
||||
* @returns {void}
|
||||
* @throws {Error} If validation fails
|
||||
*/
|
||||
export function validateFileOperations({
|
||||
existingTasks,
|
||||
targetTag,
|
||||
append,
|
||||
force,
|
||||
isMCP,
|
||||
logger
|
||||
}) {
|
||||
const hasExistingTasks = existingTasks.length > 0;
|
||||
|
||||
if (!hasExistingTasks) {
|
||||
logger.report(
|
||||
`Tag '${targetTag}' is empty or doesn't exist. Creating/updating tag with new tasks.`,
|
||||
'info'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (append) {
|
||||
logger.report(
|
||||
`Append mode enabled. Found ${existingTasks.length} existing tasks in tag '${targetTag}'.`,
|
||||
'info'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!force) {
|
||||
const errorMessage = `Tag '${targetTag}' already contains ${existingTasks.length} tasks. Use --force to overwrite or --append to add to existing tasks.`;
|
||||
logger.report(errorMessage, 'error');
|
||||
|
||||
if (isMCP) {
|
||||
throw new Error(errorMessage);
|
||||
} else {
|
||||
console.error(chalk.red(errorMessage));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
logger.report(
|
||||
`Force flag enabled. Overwriting existing tasks in tag '${targetTag}'.`,
|
||||
'debug'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process and transform tasks with ID remapping
|
||||
* @param {Array} rawTasks - Raw tasks from AI
|
||||
* @param {number} startId - Starting ID for new tasks
|
||||
* @param {Array} existingTasks - Existing tasks for dependency validation
|
||||
* @param {string} defaultPriority - Default priority for tasks
|
||||
* @returns {Array} Processed tasks with remapped IDs
|
||||
*/
|
||||
export function processTasks(
|
||||
rawTasks,
|
||||
startId,
|
||||
existingTasks,
|
||||
defaultPriority
|
||||
) {
|
||||
let currentId = startId;
|
||||
const taskMap = new Map();
|
||||
|
||||
// First pass: assign new IDs and create mapping
|
||||
const processedTasks = rawTasks.map((task) => {
|
||||
const newId = currentId++;
|
||||
taskMap.set(task.id, newId);
|
||||
|
||||
return {
|
||||
...task,
|
||||
id: newId,
|
||||
status: task.status || 'pending',
|
||||
priority: task.priority || defaultPriority,
|
||||
dependencies: Array.isArray(task.dependencies) ? task.dependencies : [],
|
||||
subtasks: task.subtasks || [],
|
||||
// Ensure all required fields have values
|
||||
title: task.title || '',
|
||||
description: task.description || '',
|
||||
details: task.details || '',
|
||||
testStrategy: task.testStrategy || ''
|
||||
};
|
||||
});
|
||||
|
||||
// Second pass: remap dependencies
|
||||
processedTasks.forEach((task) => {
|
||||
task.dependencies = task.dependencies
|
||||
.map((depId) => taskMap.get(depId))
|
||||
.filter(
|
||||
(newDepId) =>
|
||||
newDepId != null &&
|
||||
newDepId < task.id &&
|
||||
(findTaskById(existingTasks, newDepId) ||
|
||||
processedTasks.some((t) => t.id === newDepId))
|
||||
);
|
||||
});
|
||||
|
||||
return processedTasks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save tasks to file with tag support
|
||||
* @param {string} tasksPath - Path to save tasks
|
||||
* @param {Array} tasks - Tasks to save
|
||||
* @param {string} targetTag - Target tag
|
||||
* @param {Object} logger - Logger instance
|
||||
*/
|
||||
export function saveTasksToFile(tasksPath, tasks, targetTag, logger) {
|
||||
// Create directory if it doesn't exist
|
||||
const tasksDir = path.dirname(tasksPath);
|
||||
if (!fs.existsSync(tasksDir)) {
|
||||
fs.mkdirSync(tasksDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Read existing file to preserve other tags
|
||||
let outputData = {};
|
||||
if (fs.existsSync(tasksPath)) {
|
||||
try {
|
||||
const existingFileContent = fs.readFileSync(tasksPath, 'utf8');
|
||||
outputData = JSON.parse(existingFileContent);
|
||||
} catch (error) {
|
||||
outputData = {};
|
||||
}
|
||||
}
|
||||
|
||||
// Update only the target tag
|
||||
outputData[targetTag] = {
|
||||
tasks: tasks,
|
||||
metadata: {
|
||||
created:
|
||||
outputData[targetTag]?.metadata?.created || new Date().toISOString(),
|
||||
updated: new Date().toISOString(),
|
||||
description: `Tasks for ${targetTag} context`
|
||||
}
|
||||
};
|
||||
|
||||
// Ensure proper metadata
|
||||
ensureTagMetadata(outputData[targetTag], {
|
||||
description: `Tasks for ${targetTag} context`
|
||||
});
|
||||
|
||||
// Write back to file
|
||||
fs.writeFileSync(tasksPath, JSON.stringify(outputData, null, 2));
|
||||
|
||||
logger.report(
|
||||
`Successfully saved ${tasks.length} tasks to ${tasksPath}`,
|
||||
'debug'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build prompts for AI service
|
||||
* @param {Object} config - Configuration object
|
||||
* @param {string} prdContent - PRD content
|
||||
* @param {number} nextId - Next task ID
|
||||
* @returns {Promise<{systemPrompt: string, userPrompt: string}>}
|
||||
*/
|
||||
export async function buildPrompts(config, prdContent, nextId) {
|
||||
const promptManager = getPromptManager();
|
||||
const defaultTaskPriority =
|
||||
getDefaultPriority(config.projectRoot) || 'medium';
|
||||
|
||||
return promptManager.loadPrompt('parse-prd', {
|
||||
research: config.research,
|
||||
numTasks: config.numTasks,
|
||||
nextId,
|
||||
prdContent,
|
||||
prdPath: config.prdPath,
|
||||
defaultTaskPriority,
|
||||
hasCodebaseAnalysis: config.hasCodebaseAnalysis(),
|
||||
projectRoot: config.projectRoot || ''
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle progress reporting for both CLI and MCP
|
||||
* @param {Object} params
|
||||
*/
|
||||
export async function reportTaskProgress({
|
||||
task,
|
||||
currentCount,
|
||||
totalTasks,
|
||||
estimatedTokens,
|
||||
progressTracker,
|
||||
reportProgress,
|
||||
priorityMap,
|
||||
defaultPriority,
|
||||
estimatedInputTokens
|
||||
}) {
|
||||
const priority = task.priority || defaultPriority;
|
||||
const priorityIndicator = priorityMap[priority] || priorityMap.medium;
|
||||
|
||||
// CLI progress tracker
|
||||
if (progressTracker) {
|
||||
progressTracker.addTaskLine(currentCount, task.title, priority);
|
||||
if (estimatedTokens) {
|
||||
progressTracker.updateTokens(estimatedInputTokens, estimatedTokens);
|
||||
}
|
||||
}
|
||||
|
||||
// MCP progress reporting
|
||||
if (reportProgress) {
|
||||
try {
|
||||
const outputTokens = estimatedTokens
|
||||
? Math.floor(estimatedTokens / totalTasks)
|
||||
: 0;
|
||||
|
||||
await reportProgress({
|
||||
progress: currentCount,
|
||||
total: totalTasks,
|
||||
message: `${priorityIndicator} Task ${currentCount}/${totalTasks} - ${task.title} | ~Output: ${outputTokens} tokens`
|
||||
});
|
||||
} catch (error) {
|
||||
// Ignore progress reporting errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display completion summary for CLI
|
||||
* @param {Object} params
|
||||
*/
|
||||
export async function displayCliSummary({
|
||||
processedTasks,
|
||||
nextId,
|
||||
summary,
|
||||
prdPath,
|
||||
tasksPath,
|
||||
usedFallback,
|
||||
aiServiceResponse
|
||||
}) {
|
||||
// Generate task file names
|
||||
const taskFilesGenerated = (() => {
|
||||
if (!Array.isArray(processedTasks) || processedTasks.length === 0) {
|
||||
return `task_${String(nextId).padStart(3, '0')}.txt`;
|
||||
}
|
||||
const firstNewTaskId = processedTasks[0].id;
|
||||
const lastNewTaskId = processedTasks[processedTasks.length - 1].id;
|
||||
if (processedTasks.length === 1) {
|
||||
return `task_${String(firstNewTaskId).padStart(3, '0')}.txt`;
|
||||
}
|
||||
return `task_${String(firstNewTaskId).padStart(3, '0')}.txt -> task_${String(lastNewTaskId).padStart(3, '0')}.txt`;
|
||||
})();
|
||||
|
||||
displayParsePrdSummary({
|
||||
totalTasks: processedTasks.length,
|
||||
taskPriorities: summary.taskPriorities,
|
||||
prdFilePath: prdPath,
|
||||
outputPath: tasksPath,
|
||||
elapsedTime: summary.elapsedTime,
|
||||
usedFallback,
|
||||
taskFilesGenerated,
|
||||
actionVerb: summary.actionVerb
|
||||
});
|
||||
|
||||
// Display telemetry
|
||||
if (aiServiceResponse?.telemetryData) {
|
||||
// For streaming, wait briefly to allow usage data to be captured
|
||||
if (aiServiceResponse.mainResult?.usage) {
|
||||
// Give the usage promise a short time to resolve
|
||||
await TimeoutManager.withSoftTimeout(
|
||||
aiServiceResponse.mainResult.usage,
|
||||
1000,
|
||||
undefined
|
||||
);
|
||||
}
|
||||
displayAiUsageSummary(aiServiceResponse.telemetryData, 'cli');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display non-streaming CLI output
|
||||
* @param {Object} params
|
||||
*/
|
||||
export function displayNonStreamingCliOutput({
|
||||
processedTasks,
|
||||
research,
|
||||
finalTasks,
|
||||
tasksPath,
|
||||
aiServiceResponse
|
||||
}) {
|
||||
console.log(
|
||||
boxen(
|
||||
chalk.green(
|
||||
`Successfully generated ${processedTasks.length} new tasks${research ? ' with research-backed analysis' : ''}. Total tasks in ${tasksPath}: ${finalTasks.length}`
|
||||
),
|
||||
{ padding: 1, borderColor: 'green', borderStyle: 'round' }
|
||||
)
|
||||
);
|
||||
|
||||
console.log(
|
||||
boxen(
|
||||
chalk.white.bold('Next Steps:') +
|
||||
'\n\n' +
|
||||
`${chalk.cyan('1.')} Run ${chalk.yellow('task-master list')} to view all tasks\n` +
|
||||
`${chalk.cyan('2.')} Run ${chalk.yellow('task-master expand --id=<id>')} to break down a task into subtasks`,
|
||||
{
|
||||
padding: 1,
|
||||
borderColor: 'cyan',
|
||||
borderStyle: 'round',
|
||||
margin: { top: 1 }
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
if (aiServiceResponse?.telemetryData) {
|
||||
displayAiUsageSummary(aiServiceResponse.telemetryData, 'cli');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Non-streaming handler for PRD parsing
|
||||
*/
|
||||
|
||||
import ora from 'ora';
|
||||
import { generateObjectService } from '../../ai-services-unified.js';
|
||||
import { LoggingConfig, prdResponseSchema } from './parse-prd-config.js';
|
||||
import { estimateTokens } from './parse-prd-helpers.js';
|
||||
|
||||
/**
|
||||
* Handle non-streaming AI service call
|
||||
* @param {Object} config - Configuration object
|
||||
* @param {Object} prompts - System and user prompts
|
||||
* @returns {Promise<Object>} Generated tasks and telemetry
|
||||
*/
|
||||
export async function handleNonStreamingService(config, prompts) {
|
||||
const logger = new LoggingConfig(config.mcpLog, config.reportProgress);
|
||||
const { systemPrompt, userPrompt } = prompts;
|
||||
const estimatedInputTokens = estimateTokens(systemPrompt + userPrompt);
|
||||
|
||||
// Initialize spinner for CLI
|
||||
let spinner = null;
|
||||
if (config.outputFormat === 'text' && !config.isMCP) {
|
||||
spinner = ora('Parsing PRD and generating tasks...\n').start();
|
||||
}
|
||||
|
||||
try {
|
||||
// Call AI service
|
||||
logger.report(
|
||||
`Calling AI service to generate tasks from PRD${config.research ? ' with research-backed analysis' : ''}...`,
|
||||
'info'
|
||||
);
|
||||
|
||||
const aiServiceResponse = await generateObjectService({
|
||||
role: config.research ? 'research' : 'main',
|
||||
session: config.session,
|
||||
projectRoot: config.projectRoot,
|
||||
schema: prdResponseSchema,
|
||||
objectName: 'tasks_data',
|
||||
systemPrompt,
|
||||
prompt: userPrompt,
|
||||
commandName: 'parse-prd',
|
||||
outputType: config.isMCP ? 'mcp' : 'cli'
|
||||
});
|
||||
|
||||
// Extract generated data
|
||||
let generatedData = null;
|
||||
if (aiServiceResponse?.mainResult) {
|
||||
if (
|
||||
typeof aiServiceResponse.mainResult === 'object' &&
|
||||
aiServiceResponse.mainResult !== null &&
|
||||
'tasks' in aiServiceResponse.mainResult
|
||||
) {
|
||||
generatedData = aiServiceResponse.mainResult;
|
||||
} else if (
|
||||
typeof aiServiceResponse.mainResult.object === 'object' &&
|
||||
aiServiceResponse.mainResult.object !== null &&
|
||||
'tasks' in aiServiceResponse.mainResult.object
|
||||
) {
|
||||
generatedData = aiServiceResponse.mainResult.object;
|
||||
}
|
||||
}
|
||||
|
||||
if (!generatedData || !Array.isArray(generatedData.tasks)) {
|
||||
throw new Error(
|
||||
'AI service returned unexpected data structure after validation.'
|
||||
);
|
||||
}
|
||||
|
||||
if (spinner) {
|
||||
spinner.succeed('Tasks generated successfully!');
|
||||
}
|
||||
|
||||
return {
|
||||
parsedTasks: generatedData.tasks,
|
||||
aiServiceResponse,
|
||||
estimatedInputTokens
|
||||
};
|
||||
} catch (error) {
|
||||
if (spinner) {
|
||||
spinner.fail(`Error parsing PRD: ${error.message}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
653
scripts/modules/task-manager/parse-prd/parse-prd-streaming.js
Normal file
653
scripts/modules/task-manager/parse-prd/parse-prd-streaming.js
Normal file
@@ -0,0 +1,653 @@
|
||||
/**
|
||||
* Streaming handler for PRD parsing
|
||||
*/
|
||||
|
||||
import { createParsePrdTracker } from '../../../../src/progress/parse-prd-tracker.js';
|
||||
import { displayParsePrdStart } from '../../../../src/ui/parse-prd.js';
|
||||
import { getPriorityIndicators } from '../../../../src/ui/indicators.js';
|
||||
import { TimeoutManager } from '../../../../src/utils/timeout-manager.js';
|
||||
import {
|
||||
streamObjectService,
|
||||
generateObjectService
|
||||
} from '../../ai-services-unified.js';
|
||||
import {
|
||||
getMainModelId,
|
||||
getParametersForRole,
|
||||
getResearchModelId,
|
||||
getDefaultPriority
|
||||
} from '../../config-manager.js';
|
||||
import { LoggingConfig, prdResponseSchema } from './parse-prd-config.js';
|
||||
import { estimateTokens, reportTaskProgress } from './parse-prd-helpers.js';
|
||||
|
||||
/**
|
||||
* Extract a readable stream from various stream result formats
|
||||
* @param {any} streamResult - The stream result object from AI service
|
||||
* @returns {AsyncIterable|ReadableStream} The extracted stream
|
||||
* @throws {StreamingError} If no valid stream can be extracted
|
||||
*/
|
||||
function extractStreamFromResult(streamResult) {
|
||||
if (!streamResult) {
|
||||
throw new StreamingError(
|
||||
'Stream result is null or undefined',
|
||||
STREAMING_ERROR_CODES.NOT_ASYNC_ITERABLE
|
||||
);
|
||||
}
|
||||
|
||||
// Try extraction strategies in priority order
|
||||
const stream = tryExtractStream(streamResult);
|
||||
|
||||
if (!stream) {
|
||||
throw new StreamingError(
|
||||
'Stream object is not async iterable or readable',
|
||||
STREAMING_ERROR_CODES.NOT_ASYNC_ITERABLE
|
||||
);
|
||||
}
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to extract stream using various strategies
|
||||
*/
|
||||
function tryExtractStream(streamResult) {
|
||||
const streamExtractors = [
|
||||
{ key: 'partialObjectStream', extractor: (obj) => obj.partialObjectStream },
|
||||
{ key: 'textStream', extractor: (obj) => extractCallable(obj.textStream) },
|
||||
{ key: 'stream', extractor: (obj) => extractCallable(obj.stream) },
|
||||
{ key: 'baseStream', extractor: (obj) => obj.baseStream }
|
||||
];
|
||||
|
||||
for (const { key, extractor } of streamExtractors) {
|
||||
const stream = extractor(streamResult);
|
||||
if (stream && isStreamable(stream)) {
|
||||
return stream;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if already streamable
|
||||
return isStreamable(streamResult) ? streamResult : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a property that might be a function or direct value
|
||||
*/
|
||||
function extractCallable(property) {
|
||||
if (!property) return null;
|
||||
return typeof property === 'function' ? property() : property;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if object is streamable (async iterable or readable stream)
|
||||
*/
|
||||
function isStreamable(obj) {
|
||||
return (
|
||||
obj &&
|
||||
(typeof obj[Symbol.asyncIterator] === 'function' ||
|
||||
(obj.getReader && typeof obj.getReader === 'function'))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle streaming AI service call and parsing
|
||||
* @param {Object} config - Configuration object
|
||||
* @param {Object} prompts - System and user prompts
|
||||
* @param {number} numTasks - Number of tasks to generate
|
||||
* @returns {Promise<Object>} Parsed tasks and telemetry
|
||||
*/
|
||||
export async function handleStreamingService(config, prompts, numTasks) {
|
||||
const context = createStreamingContext(config, prompts, numTasks);
|
||||
|
||||
await initializeProgress(config, numTasks, context.estimatedInputTokens);
|
||||
|
||||
const aiServiceResponse = await callAIServiceWithTimeout(
|
||||
config,
|
||||
prompts,
|
||||
config.streamingTimeout
|
||||
);
|
||||
|
||||
const { progressTracker, priorityMap } = await setupProgressTracking(
|
||||
config,
|
||||
numTasks
|
||||
);
|
||||
|
||||
const streamingResult = await processStreamResponse(
|
||||
aiServiceResponse.mainResult,
|
||||
config,
|
||||
prompts,
|
||||
numTasks,
|
||||
progressTracker,
|
||||
priorityMap,
|
||||
context.defaultPriority,
|
||||
context.estimatedInputTokens,
|
||||
context.logger
|
||||
);
|
||||
|
||||
validateStreamingResult(streamingResult);
|
||||
|
||||
// If we have usage data from streaming, log telemetry now
|
||||
if (streamingResult.usage && config.projectRoot) {
|
||||
const { logAiUsage } = await import('../../ai-services-unified.js');
|
||||
const { getUserId } = await import('../../config-manager.js');
|
||||
const userId = getUserId(config.projectRoot);
|
||||
|
||||
if (userId && aiServiceResponse.providerName && aiServiceResponse.modelId) {
|
||||
try {
|
||||
const telemetryData = await logAiUsage({
|
||||
userId,
|
||||
commandName: 'parse-prd',
|
||||
providerName: aiServiceResponse.providerName,
|
||||
modelId: aiServiceResponse.modelId,
|
||||
inputTokens: streamingResult.usage.promptTokens || 0,
|
||||
outputTokens: streamingResult.usage.completionTokens || 0,
|
||||
outputType: config.isMCP ? 'mcp' : 'cli'
|
||||
});
|
||||
|
||||
// Add telemetry to the response
|
||||
if (telemetryData) {
|
||||
aiServiceResponse.telemetryData = telemetryData;
|
||||
}
|
||||
} catch (telemetryError) {
|
||||
context.logger.report(
|
||||
`Failed to log telemetry: ${telemetryError.message}`,
|
||||
'debug'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return prepareFinalResult(
|
||||
streamingResult,
|
||||
aiServiceResponse,
|
||||
context.estimatedInputTokens,
|
||||
progressTracker
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create streaming context with common values
|
||||
*/
|
||||
function createStreamingContext(config, prompts, numTasks) {
|
||||
const { systemPrompt, userPrompt } = prompts;
|
||||
return {
|
||||
logger: new LoggingConfig(config.mcpLog, config.reportProgress),
|
||||
estimatedInputTokens: estimateTokens(systemPrompt + userPrompt),
|
||||
defaultPriority: getDefaultPriority(config.projectRoot) || 'medium'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate streaming result has tasks
|
||||
*/
|
||||
function validateStreamingResult(streamingResult) {
|
||||
if (streamingResult.parsedTasks.length === 0) {
|
||||
throw new Error('No tasks were generated from the PRD');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize progress reporting
|
||||
*/
|
||||
async function initializeProgress(config, numTasks, estimatedInputTokens) {
|
||||
if (config.reportProgress) {
|
||||
await config.reportProgress({
|
||||
progress: 0,
|
||||
total: numTasks,
|
||||
message: `Starting PRD analysis (Input: ${estimatedInputTokens} tokens)${config.research ? ' with research' : ''}...`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call AI service with timeout
|
||||
*/
|
||||
async function callAIServiceWithTimeout(config, prompts, timeout) {
|
||||
const { systemPrompt, userPrompt } = prompts;
|
||||
|
||||
return await TimeoutManager.withTimeout(
|
||||
streamObjectService({
|
||||
role: config.research ? 'research' : 'main',
|
||||
session: config.session,
|
||||
projectRoot: config.projectRoot,
|
||||
schema: prdResponseSchema,
|
||||
systemPrompt,
|
||||
prompt: userPrompt,
|
||||
commandName: 'parse-prd',
|
||||
outputType: config.isMCP ? 'mcp' : 'cli'
|
||||
}),
|
||||
timeout,
|
||||
'Streaming operation'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup progress tracking for CLI output
|
||||
*/
|
||||
async function setupProgressTracking(config, numTasks) {
|
||||
const priorityMap = getPriorityIndicators(config.isMCP);
|
||||
let progressTracker = null;
|
||||
|
||||
if (config.outputFormat === 'text' && !config.isMCP) {
|
||||
progressTracker = createParsePrdTracker({
|
||||
numUnits: numTasks,
|
||||
unitName: 'task',
|
||||
append: config.append
|
||||
});
|
||||
|
||||
const modelId = config.research ? getResearchModelId() : getMainModelId();
|
||||
const parameters = getParametersForRole(
|
||||
config.research ? 'research' : 'main'
|
||||
);
|
||||
|
||||
displayParsePrdStart({
|
||||
prdFilePath: config.prdPath,
|
||||
outputPath: config.tasksPath,
|
||||
numTasks,
|
||||
append: config.append,
|
||||
research: config.research,
|
||||
force: config.force,
|
||||
existingTasks: [],
|
||||
nextId: 1,
|
||||
model: modelId || 'Default',
|
||||
temperature: parameters?.temperature || 0.7
|
||||
});
|
||||
|
||||
progressTracker.start();
|
||||
}
|
||||
|
||||
return { progressTracker, priorityMap };
|
||||
}
|
||||
|
||||
/**
|
||||
* Process stream response based on stream type
|
||||
*/
|
||||
async function processStreamResponse(
|
||||
streamResult,
|
||||
config,
|
||||
prompts,
|
||||
numTasks,
|
||||
progressTracker,
|
||||
priorityMap,
|
||||
defaultPriority,
|
||||
estimatedInputTokens,
|
||||
logger
|
||||
) {
|
||||
const { systemPrompt, userPrompt } = prompts;
|
||||
const context = {
|
||||
config: {
|
||||
...config,
|
||||
schema: prdResponseSchema // Add the schema for generateObject fallback
|
||||
},
|
||||
numTasks,
|
||||
progressTracker,
|
||||
priorityMap,
|
||||
defaultPriority,
|
||||
estimatedInputTokens,
|
||||
prompt: userPrompt,
|
||||
systemPrompt: systemPrompt
|
||||
};
|
||||
|
||||
try {
|
||||
const streamingState = {
|
||||
lastPartialObject: null,
|
||||
taskCount: 0,
|
||||
estimatedOutputTokens: 0,
|
||||
usage: null
|
||||
};
|
||||
|
||||
await processPartialStream(
|
||||
streamResult.partialObjectStream,
|
||||
streamingState,
|
||||
context
|
||||
);
|
||||
|
||||
// Wait for usage data if available
|
||||
if (streamResult.usage) {
|
||||
try {
|
||||
streamingState.usage = await streamResult.usage;
|
||||
} catch (usageError) {
|
||||
logger.report(
|
||||
`Failed to get usage data: ${usageError.message}`,
|
||||
'debug'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return finalizeStreamingResults(streamingState, context);
|
||||
} catch (error) {
|
||||
logger.report(
|
||||
`StreamObject processing failed: ${error.message}. Falling back to generateObject.`,
|
||||
'debug'
|
||||
);
|
||||
return await processWithGenerateObject(context, logger);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the partial object stream
|
||||
*/
|
||||
async function processPartialStream(partialStream, state, context) {
|
||||
for await (const partialObject of partialStream) {
|
||||
state.lastPartialObject = partialObject;
|
||||
|
||||
if (partialObject) {
|
||||
state.estimatedOutputTokens = estimateTokens(
|
||||
JSON.stringify(partialObject)
|
||||
);
|
||||
}
|
||||
|
||||
await processStreamingTasks(partialObject, state, context);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process tasks from a streaming partial object
|
||||
*/
|
||||
async function processStreamingTasks(partialObject, state, context) {
|
||||
if (!partialObject?.tasks || !Array.isArray(partialObject.tasks)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newTaskCount = partialObject.tasks.length;
|
||||
|
||||
if (newTaskCount > state.taskCount) {
|
||||
await processNewTasks(
|
||||
partialObject.tasks,
|
||||
state.taskCount,
|
||||
newTaskCount,
|
||||
state.estimatedOutputTokens,
|
||||
context
|
||||
);
|
||||
state.taskCount = newTaskCount;
|
||||
} else if (context.progressTracker && state.estimatedOutputTokens > 0) {
|
||||
context.progressTracker.updateTokens(
|
||||
context.estimatedInputTokens,
|
||||
state.estimatedOutputTokens,
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process newly appeared tasks in the stream
|
||||
*/
|
||||
async function processNewTasks(
|
||||
tasks,
|
||||
startIndex,
|
||||
endIndex,
|
||||
estimatedOutputTokens,
|
||||
context
|
||||
) {
|
||||
for (let i = startIndex; i < endIndex; i++) {
|
||||
const task = tasks[i] || {};
|
||||
|
||||
if (task.title) {
|
||||
await reportTaskProgress({
|
||||
task,
|
||||
currentCount: i + 1,
|
||||
totalTasks: context.numTasks,
|
||||
estimatedTokens: estimatedOutputTokens,
|
||||
progressTracker: context.progressTracker,
|
||||
reportProgress: context.config.reportProgress,
|
||||
priorityMap: context.priorityMap,
|
||||
defaultPriority: context.defaultPriority,
|
||||
estimatedInputTokens: context.estimatedInputTokens
|
||||
});
|
||||
} else {
|
||||
await reportPlaceholderTask(i + 1, estimatedOutputTokens, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Report a placeholder task while it's being generated
|
||||
*/
|
||||
async function reportPlaceholderTask(
|
||||
taskNumber,
|
||||
estimatedOutputTokens,
|
||||
context
|
||||
) {
|
||||
const {
|
||||
progressTracker,
|
||||
config,
|
||||
numTasks,
|
||||
defaultPriority,
|
||||
estimatedInputTokens
|
||||
} = context;
|
||||
|
||||
if (progressTracker) {
|
||||
progressTracker.addTaskLine(
|
||||
taskNumber,
|
||||
`Generating task ${taskNumber}...`,
|
||||
defaultPriority
|
||||
);
|
||||
progressTracker.updateTokens(
|
||||
estimatedInputTokens,
|
||||
estimatedOutputTokens,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
if (config.reportProgress && !progressTracker) {
|
||||
await config.reportProgress({
|
||||
progress: taskNumber,
|
||||
total: numTasks,
|
||||
message: `Generating task ${taskNumber}/${numTasks}...`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finalize streaming results and update progress display
|
||||
*/
|
||||
async function finalizeStreamingResults(state, context) {
|
||||
const { lastPartialObject, estimatedOutputTokens, taskCount, usage } = state;
|
||||
|
||||
if (!lastPartialObject?.tasks || !Array.isArray(lastPartialObject.tasks)) {
|
||||
throw new Error('No tasks generated from streamObject');
|
||||
}
|
||||
|
||||
// Use actual token counts if available, otherwise use estimates
|
||||
const finalOutputTokens = usage?.completionTokens || estimatedOutputTokens;
|
||||
const finalInputTokens = usage?.promptTokens || context.estimatedInputTokens;
|
||||
|
||||
if (context.progressTracker) {
|
||||
await updateFinalProgress(
|
||||
lastPartialObject.tasks,
|
||||
taskCount,
|
||||
usage ? finalOutputTokens : estimatedOutputTokens,
|
||||
context,
|
||||
usage ? finalInputTokens : null
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
parsedTasks: lastPartialObject.tasks,
|
||||
estimatedOutputTokens: finalOutputTokens,
|
||||
actualInputTokens: finalInputTokens,
|
||||
usage,
|
||||
usedFallback: false
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update progress tracker with final task content
|
||||
*/
|
||||
async function updateFinalProgress(
|
||||
tasks,
|
||||
taskCount,
|
||||
outputTokens,
|
||||
context,
|
||||
actualInputTokens = null
|
||||
) {
|
||||
const { progressTracker, defaultPriority, estimatedInputTokens } = context;
|
||||
|
||||
if (taskCount > 0) {
|
||||
updateTaskLines(tasks, progressTracker, defaultPriority);
|
||||
} else {
|
||||
await reportAllTasks(tasks, outputTokens, context);
|
||||
}
|
||||
|
||||
progressTracker.updateTokens(
|
||||
actualInputTokens || estimatedInputTokens,
|
||||
outputTokens,
|
||||
false
|
||||
);
|
||||
progressTracker.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update task lines in progress tracker with final content
|
||||
*/
|
||||
function updateTaskLines(tasks, progressTracker, defaultPriority) {
|
||||
for (let i = 0; i < tasks.length; i++) {
|
||||
const task = tasks[i];
|
||||
if (task?.title) {
|
||||
progressTracker.addTaskLine(
|
||||
i + 1,
|
||||
task.title,
|
||||
task.priority || defaultPriority
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Report all tasks that were not streamed incrementally
|
||||
*/
|
||||
async function reportAllTasks(tasks, estimatedOutputTokens, context) {
|
||||
for (let i = 0; i < tasks.length; i++) {
|
||||
const task = tasks[i];
|
||||
if (task?.title) {
|
||||
await reportTaskProgress({
|
||||
task,
|
||||
currentCount: i + 1,
|
||||
totalTasks: context.numTasks,
|
||||
estimatedTokens: estimatedOutputTokens,
|
||||
progressTracker: context.progressTracker,
|
||||
reportProgress: context.config.reportProgress,
|
||||
priorityMap: context.priorityMap,
|
||||
defaultPriority: context.defaultPriority,
|
||||
estimatedInputTokens: context.estimatedInputTokens
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process with generateObject as fallback when streaming fails
|
||||
*/
|
||||
async function processWithGenerateObject(context, logger) {
|
||||
logger.report('Using generateObject fallback for PRD parsing', 'info');
|
||||
|
||||
// Show placeholder tasks while generating
|
||||
if (context.progressTracker) {
|
||||
for (let i = 0; i < context.numTasks; i++) {
|
||||
context.progressTracker.addTaskLine(
|
||||
i + 1,
|
||||
`Generating task ${i + 1}...`,
|
||||
context.defaultPriority
|
||||
);
|
||||
context.progressTracker.updateTokens(
|
||||
context.estimatedInputTokens,
|
||||
0,
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Use generateObjectService instead of streaming
|
||||
const result = await generateObjectService({
|
||||
role: context.config.research ? 'research' : 'main',
|
||||
commandName: 'parse-prd',
|
||||
prompt: context.prompt,
|
||||
systemPrompt: context.systemPrompt,
|
||||
schema: context.config.schema,
|
||||
outputFormat: context.config.outputFormat || 'text',
|
||||
projectRoot: context.config.projectRoot,
|
||||
session: context.config.session
|
||||
});
|
||||
|
||||
// Extract tasks from the result (handle both direct tasks and mainResult.tasks)
|
||||
const tasks = result?.mainResult || result;
|
||||
|
||||
// Process the generated tasks
|
||||
if (tasks && Array.isArray(tasks.tasks)) {
|
||||
// Update progress tracker with final tasks
|
||||
if (context.progressTracker) {
|
||||
for (let i = 0; i < tasks.tasks.length; i++) {
|
||||
const task = tasks.tasks[i];
|
||||
if (task && task.title) {
|
||||
context.progressTracker.addTaskLine(
|
||||
i + 1,
|
||||
task.title,
|
||||
task.priority || context.defaultPriority
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Final token update - use actual telemetry if available
|
||||
const outputTokens =
|
||||
result.telemetryData?.outputTokens ||
|
||||
estimateTokens(JSON.stringify(tasks));
|
||||
const inputTokens =
|
||||
result.telemetryData?.inputTokens || context.estimatedInputTokens;
|
||||
|
||||
context.progressTracker.updateTokens(inputTokens, outputTokens, false);
|
||||
}
|
||||
|
||||
return {
|
||||
parsedTasks: tasks.tasks,
|
||||
estimatedOutputTokens:
|
||||
result.telemetryData?.outputTokens ||
|
||||
estimateTokens(JSON.stringify(tasks)),
|
||||
actualInputTokens: result.telemetryData?.inputTokens,
|
||||
telemetryData: result.telemetryData,
|
||||
usedFallback: true
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error('Failed to generate tasks using generateObject fallback');
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare final result with cleanup
|
||||
*/
|
||||
function prepareFinalResult(
|
||||
streamingResult,
|
||||
aiServiceResponse,
|
||||
estimatedInputTokens,
|
||||
progressTracker
|
||||
) {
|
||||
let summary = null;
|
||||
if (progressTracker) {
|
||||
summary = progressTracker.getSummary();
|
||||
progressTracker.cleanup();
|
||||
}
|
||||
|
||||
// If we have actual usage data from streaming, update the AI service response
|
||||
if (streamingResult.usage && aiServiceResponse) {
|
||||
// Map the Vercel AI SDK usage format to our telemetry format
|
||||
const usage = streamingResult.usage;
|
||||
if (!aiServiceResponse.usage) {
|
||||
aiServiceResponse.usage = {
|
||||
promptTokens: usage.promptTokens || 0,
|
||||
completionTokens: usage.completionTokens || 0,
|
||||
totalTokens: usage.totalTokens || 0
|
||||
};
|
||||
}
|
||||
|
||||
// The telemetry should have been logged in the unified service runner
|
||||
// but if not, the usage is now available for telemetry calculation
|
||||
}
|
||||
|
||||
return {
|
||||
parsedTasks: streamingResult.parsedTasks,
|
||||
aiServiceResponse,
|
||||
estimatedInputTokens:
|
||||
streamingResult.actualInputTokens || estimatedInputTokens,
|
||||
estimatedOutputTokens: streamingResult.estimatedOutputTokens,
|
||||
usedFallback: streamingResult.usedFallback,
|
||||
progressTracker,
|
||||
summary
|
||||
};
|
||||
}
|
||||
272
scripts/modules/task-manager/parse-prd/parse-prd.js
Normal file
272
scripts/modules/task-manager/parse-prd/parse-prd.js
Normal file
@@ -0,0 +1,272 @@
|
||||
import chalk from 'chalk';
|
||||
import {
|
||||
StreamingError,
|
||||
STREAMING_ERROR_CODES
|
||||
} from '../../../../src/utils/stream-parser.js';
|
||||
import { TimeoutManager } from '../../../../src/utils/timeout-manager.js';
|
||||
import { getDebugFlag, getDefaultPriority } from '../../config-manager.js';
|
||||
|
||||
// Import configuration classes
|
||||
import { PrdParseConfig, LoggingConfig } from './parse-prd-config.js';
|
||||
|
||||
// Import helper functions
|
||||
import {
|
||||
readPrdContent,
|
||||
loadExistingTasks,
|
||||
validateFileOperations,
|
||||
processTasks,
|
||||
saveTasksToFile,
|
||||
buildPrompts,
|
||||
displayCliSummary,
|
||||
displayNonStreamingCliOutput
|
||||
} from './parse-prd-helpers.js';
|
||||
|
||||
// Import handlers
|
||||
import { handleStreamingService } from './parse-prd-streaming.js';
|
||||
import { handleNonStreamingService } from './parse-prd-non-streaming.js';
|
||||
|
||||
// ============================================================================
|
||||
// MAIN PARSING FUNCTIONS (Simplified after refactoring)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Shared parsing logic for both streaming and non-streaming
|
||||
* @param {PrdParseConfig} config - Configuration object
|
||||
* @param {Function} serviceHandler - Handler function for AI service
|
||||
* @param {boolean} isStreaming - Whether this is streaming mode
|
||||
* @returns {Promise<Object>} Result object with success status and telemetry
|
||||
*/
|
||||
async function parsePRDCore(config, serviceHandler, isStreaming) {
|
||||
const logger = new LoggingConfig(config.mcpLog, config.reportProgress);
|
||||
|
||||
logger.report(
|
||||
`Parsing PRD file: ${config.prdPath}, Force: ${config.force}, Append: ${config.append}, Research: ${config.research}`,
|
||||
'debug'
|
||||
);
|
||||
|
||||
try {
|
||||
// Load existing tasks
|
||||
const { existingTasks, nextId } = loadExistingTasks(
|
||||
config.tasksPath,
|
||||
config.targetTag
|
||||
);
|
||||
|
||||
// Validate operations
|
||||
validateFileOperations({
|
||||
existingTasks,
|
||||
targetTag: config.targetTag,
|
||||
append: config.append,
|
||||
force: config.force,
|
||||
isMCP: config.isMCP,
|
||||
logger
|
||||
});
|
||||
|
||||
// Read PRD content and build prompts
|
||||
const prdContent = readPrdContent(config.prdPath);
|
||||
const prompts = await buildPrompts(config, prdContent, nextId);
|
||||
|
||||
// Call the appropriate service handler
|
||||
const serviceResult = await serviceHandler(
|
||||
config,
|
||||
prompts,
|
||||
config.numTasks
|
||||
);
|
||||
|
||||
// Process tasks
|
||||
const defaultPriority = getDefaultPriority(config.projectRoot) || 'medium';
|
||||
const processedNewTasks = processTasks(
|
||||
serviceResult.parsedTasks,
|
||||
nextId,
|
||||
existingTasks,
|
||||
defaultPriority
|
||||
);
|
||||
|
||||
// Combine with existing if appending
|
||||
const finalTasks = config.append
|
||||
? [...existingTasks, ...processedNewTasks]
|
||||
: processedNewTasks;
|
||||
|
||||
// Save to file
|
||||
saveTasksToFile(config.tasksPath, finalTasks, config.targetTag, logger);
|
||||
|
||||
// Handle completion reporting
|
||||
await handleCompletionReporting(
|
||||
config,
|
||||
serviceResult,
|
||||
processedNewTasks,
|
||||
finalTasks,
|
||||
nextId,
|
||||
isStreaming
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
tasksPath: config.tasksPath,
|
||||
telemetryData: serviceResult.aiServiceResponse?.telemetryData,
|
||||
tagInfo: serviceResult.aiServiceResponse?.tagInfo
|
||||
};
|
||||
} catch (error) {
|
||||
logger.report(`Error parsing PRD: ${error.message}`, 'error');
|
||||
|
||||
if (!config.isMCP) {
|
||||
console.error(chalk.red(`Error: ${error.message}`));
|
||||
if (getDebugFlag(config.projectRoot)) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle completion reporting for both CLI and MCP
|
||||
* @param {PrdParseConfig} config - Configuration object
|
||||
* @param {Object} serviceResult - Result from service handler
|
||||
* @param {Array} processedNewTasks - New tasks that were processed
|
||||
* @param {Array} finalTasks - All tasks after processing
|
||||
* @param {number} nextId - Next available task ID
|
||||
* @param {boolean} isStreaming - Whether this was streaming mode
|
||||
*/
|
||||
async function handleCompletionReporting(
|
||||
config,
|
||||
serviceResult,
|
||||
processedNewTasks,
|
||||
finalTasks,
|
||||
nextId,
|
||||
isStreaming
|
||||
) {
|
||||
const { aiServiceResponse, estimatedInputTokens, estimatedOutputTokens } =
|
||||
serviceResult;
|
||||
|
||||
// MCP progress reporting
|
||||
if (config.reportProgress) {
|
||||
const hasValidTelemetry =
|
||||
aiServiceResponse?.telemetryData &&
|
||||
(aiServiceResponse.telemetryData.inputTokens > 0 ||
|
||||
aiServiceResponse.telemetryData.outputTokens > 0);
|
||||
|
||||
let completionMessage;
|
||||
if (hasValidTelemetry) {
|
||||
const cost = aiServiceResponse.telemetryData.totalCost || 0;
|
||||
const currency = aiServiceResponse.telemetryData.currency || 'USD';
|
||||
completionMessage = `✅ Task Generation Completed | Tokens (I/O): ${aiServiceResponse.telemetryData.inputTokens}/${aiServiceResponse.telemetryData.outputTokens} | Cost: ${currency === 'USD' ? '$' : currency}${cost.toFixed(4)}`;
|
||||
} else {
|
||||
const outputTokens = isStreaming ? estimatedOutputTokens : 'unknown';
|
||||
completionMessage = `✅ Task Generation Completed | ~Tokens (I/O): ${estimatedInputTokens}/${outputTokens} | Cost: ~$0.00`;
|
||||
}
|
||||
|
||||
await config.reportProgress({
|
||||
progress: config.numTasks,
|
||||
total: config.numTasks,
|
||||
message: completionMessage
|
||||
});
|
||||
}
|
||||
|
||||
// CLI output
|
||||
if (config.outputFormat === 'text' && !config.isMCP) {
|
||||
if (isStreaming && serviceResult.summary) {
|
||||
await displayCliSummary({
|
||||
processedTasks: processedNewTasks,
|
||||
nextId,
|
||||
summary: serviceResult.summary,
|
||||
prdPath: config.prdPath,
|
||||
tasksPath: config.tasksPath,
|
||||
usedFallback: serviceResult.usedFallback,
|
||||
aiServiceResponse
|
||||
});
|
||||
} else if (!isStreaming) {
|
||||
displayNonStreamingCliOutput({
|
||||
processedTasks: processedNewTasks,
|
||||
research: config.research,
|
||||
finalTasks,
|
||||
tasksPath: config.tasksPath,
|
||||
aiServiceResponse
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse PRD with streaming progress reporting
|
||||
*/
|
||||
async function parsePRDWithStreaming(
|
||||
prdPath,
|
||||
tasksPath,
|
||||
numTasks,
|
||||
options = {}
|
||||
) {
|
||||
const config = new PrdParseConfig(prdPath, tasksPath, numTasks, options);
|
||||
return parsePRDCore(config, handleStreamingService, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse PRD without streaming (fallback)
|
||||
*/
|
||||
async function parsePRDWithoutStreaming(
|
||||
prdPath,
|
||||
tasksPath,
|
||||
numTasks,
|
||||
options = {}
|
||||
) {
|
||||
const config = new PrdParseConfig(prdPath, tasksPath, numTasks, options);
|
||||
return parsePRDCore(config, handleNonStreamingService, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main entry point - decides between streaming and non-streaming
|
||||
*/
|
||||
async function parsePRD(prdPath, tasksPath, numTasks, options = {}) {
|
||||
const config = new PrdParseConfig(prdPath, tasksPath, numTasks, options);
|
||||
|
||||
if (config.useStreaming) {
|
||||
try {
|
||||
return await parsePRDWithStreaming(prdPath, tasksPath, numTasks, options);
|
||||
} catch (streamingError) {
|
||||
// Check if this is a streaming-specific error (including timeout)
|
||||
const isStreamingError =
|
||||
streamingError instanceof StreamingError ||
|
||||
streamingError.code === STREAMING_ERROR_CODES.NOT_ASYNC_ITERABLE ||
|
||||
streamingError.code ===
|
||||
STREAMING_ERROR_CODES.STREAM_PROCESSING_FAILED ||
|
||||
streamingError.code === STREAMING_ERROR_CODES.STREAM_NOT_ITERABLE ||
|
||||
TimeoutManager.isTimeoutError(streamingError);
|
||||
|
||||
if (isStreamingError) {
|
||||
const logger = new LoggingConfig(config.mcpLog, config.reportProgress);
|
||||
|
||||
// Show fallback message
|
||||
if (config.outputFormat === 'text' && !config.isMCP) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`⚠️ Streaming operation ${streamingError.message.includes('timed out') ? 'timed out' : 'failed'}. Falling back to non-streaming mode...`
|
||||
)
|
||||
);
|
||||
} else {
|
||||
logger.report(
|
||||
`Streaming failed (${streamingError.message}), falling back to non-streaming mode...`,
|
||||
'warn'
|
||||
);
|
||||
}
|
||||
|
||||
// Fallback to non-streaming
|
||||
return await parsePRDWithoutStreaming(
|
||||
prdPath,
|
||||
tasksPath,
|
||||
numTasks,
|
||||
options
|
||||
);
|
||||
} else {
|
||||
throw streamingError;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return await parsePRDWithoutStreaming(
|
||||
prdPath,
|
||||
tasksPath,
|
||||
numTasks,
|
||||
options
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default parsePRD;
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
flattenTasksWithSubtasks
|
||||
} from '../utils.js';
|
||||
import { generateTextService } from '../ai-services-unified.js';
|
||||
import { getDebugFlag } from '../config-manager.js';
|
||||
import { getDebugFlag, hasCodebaseAnalysis } from '../config-manager.js';
|
||||
import { getPromptManager } from '../prompt-manager.js';
|
||||
import generateTaskFiles from './generate-task-files.js';
|
||||
import { ContextGatherer } from '../utils/contextGatherer.js';
|
||||
@@ -231,7 +231,13 @@ async function updateSubtaskById(
|
||||
currentDetails: subtask.details || '(No existing details)',
|
||||
updatePrompt: prompt,
|
||||
useResearch: useResearch,
|
||||
gatheredContext: gatheredContext || ''
|
||||
gatheredContext: gatheredContext || '',
|
||||
hasCodebaseAnalysis: hasCodebaseAnalysis(
|
||||
useResearch,
|
||||
projectRoot,
|
||||
session
|
||||
),
|
||||
projectRoot: projectRoot
|
||||
};
|
||||
|
||||
const variantKey = useResearch ? 'research' : 'default';
|
||||
|
||||
@@ -23,7 +23,11 @@ import {
|
||||
} from '../ui.js';
|
||||
|
||||
import { generateTextService } from '../ai-services-unified.js';
|
||||
import { getDebugFlag, isApiKeySet } from '../config-manager.js';
|
||||
import {
|
||||
getDebugFlag,
|
||||
isApiKeySet,
|
||||
hasCodebaseAnalysis
|
||||
} from '../config-manager.js';
|
||||
import { getPromptManager } from '../prompt-manager.js';
|
||||
import { ContextGatherer } from '../utils/contextGatherer.js';
|
||||
import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';
|
||||
@@ -453,7 +457,13 @@ async function updateTaskById(
|
||||
appendMode: appendMode,
|
||||
useResearch: useResearch,
|
||||
currentDetails: taskToUpdate.details || '(No existing details)',
|
||||
gatheredContext: gatheredContext || ''
|
||||
gatheredContext: gatheredContext || '',
|
||||
hasCodebaseAnalysis: hasCodebaseAnalysis(
|
||||
useResearch,
|
||||
projectRoot,
|
||||
session
|
||||
),
|
||||
projectRoot: projectRoot
|
||||
};
|
||||
|
||||
const variantKey = appendMode
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
displayAiUsageSummary
|
||||
} from '../ui.js';
|
||||
|
||||
import { getDebugFlag } from '../config-manager.js';
|
||||
import { getDebugFlag, hasCodebaseAnalysis } from '../config-manager.js';
|
||||
import { getPromptManager } from '../prompt-manager.js';
|
||||
import generateTaskFiles from './generate-task-files.js';
|
||||
import { generateTextService } from '../ai-services-unified.js';
|
||||
@@ -435,7 +435,13 @@ async function updateTasks(
|
||||
tasks: tasksToUpdate,
|
||||
updatePrompt: prompt,
|
||||
useResearch,
|
||||
projectContext: gatheredContext
|
||||
projectContext: gatheredContext,
|
||||
hasCodebaseAnalysis: hasCodebaseAnalysis(
|
||||
useResearch,
|
||||
projectRoot,
|
||||
session
|
||||
),
|
||||
projectRoot: projectRoot
|
||||
}
|
||||
);
|
||||
// --- End Build Prompts ---
|
||||
|
||||
@@ -2876,9 +2876,6 @@ export function displayCrossTagDependencyError(
|
||||
` 4. Move dependencies first: task-master move --from=${conflicts.map((c) => c.dependencyId).join(',')} --from-tag=${conflicts[0].dependencyTag} --to-tag=${targetTag}`
|
||||
);
|
||||
}
|
||||
console.log(
|
||||
` 5. Force move (may break dependencies): task-master move --from=${sourceIds} --from-tag=${sourceTag} --to-tag=${targetTag} --force`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
generateObject,
|
||||
generateText,
|
||||
streamText,
|
||||
streamObject,
|
||||
zodSchema,
|
||||
JSONParseError,
|
||||
NoObjectGeneratedError
|
||||
@@ -224,6 +225,46 @@ export class BaseAIProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Streams a structured object using the provider's model
|
||||
*/
|
||||
async streamObject(params) {
|
||||
try {
|
||||
this.validateParams(params);
|
||||
this.validateMessages(params.messages);
|
||||
|
||||
if (!params.schema) {
|
||||
throw new Error('Schema is required for object streaming');
|
||||
}
|
||||
|
||||
log(
|
||||
'debug',
|
||||
`Streaming ${this.name} object with model: ${params.modelId}`
|
||||
);
|
||||
|
||||
const client = await this.getClient(params);
|
||||
const result = await streamObject({
|
||||
model: client(params.modelId),
|
||||
messages: params.messages,
|
||||
schema: zodSchema(params.schema),
|
||||
mode: params.mode || 'auto',
|
||||
maxTokens: params.maxTokens,
|
||||
temperature: params.temperature
|
||||
});
|
||||
|
||||
log(
|
||||
'debug',
|
||||
`${this.name} streamObject initiated successfully for model: ${params.modelId}`
|
||||
);
|
||||
|
||||
// Return the stream result directly
|
||||
// The stream result contains partialObjectStream and other properties
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.handleError('object streaming', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a structured object using the provider's model
|
||||
*/
|
||||
|
||||
@@ -169,6 +169,11 @@ export class ClaudeCodeLanguageModel {
|
||||
const warnings = this.generateUnsupportedWarnings(options);
|
||||
|
||||
try {
|
||||
if (!query) {
|
||||
throw new Error(
|
||||
"Claude Code SDK is not installed. Please install '@anthropic-ai/claude-code' to use the claude-code provider."
|
||||
);
|
||||
}
|
||||
const response = query({
|
||||
prompt: messagesPrompt,
|
||||
options: queryOptions
|
||||
@@ -227,7 +232,7 @@ export class ClaudeCodeLanguageModel {
|
||||
finishReason = 'truncated';
|
||||
// Skip re-throwing: fall through so the caller receives usable data
|
||||
} else {
|
||||
if (error instanceof AbortError) {
|
||||
if (AbortError && error instanceof AbortError) {
|
||||
throw options.abortSignal?.aborted
|
||||
? options.abortSignal.reason
|
||||
: error;
|
||||
@@ -335,6 +340,11 @@ export class ClaudeCodeLanguageModel {
|
||||
const stream = new ReadableStream({
|
||||
start: async (controller) => {
|
||||
try {
|
||||
if (!query) {
|
||||
throw new Error(
|
||||
"Claude Code SDK is not installed. Please install '@anthropic-ai/claude-code' to use the claude-code provider."
|
||||
);
|
||||
}
|
||||
const response = query({
|
||||
prompt: messagesPrompt,
|
||||
options: queryOptions
|
||||
@@ -478,7 +488,7 @@ export class ClaudeCodeLanguageModel {
|
||||
} catch (error) {
|
||||
let errorToEmit;
|
||||
|
||||
if (error instanceof AbortError) {
|
||||
if (AbortError && error instanceof AbortError) {
|
||||
errorToEmit = options.abortSignal?.aborted
|
||||
? options.abortSignal.reason
|
||||
: error;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @typedef {'amp' | 'claude' | 'cline' | 'codex' | 'cursor' | 'gemini' | 'kiro' | 'opencode' | 'roo' | 'trae' | 'windsurf' | 'vscode' | 'zed'} RulesProfile
|
||||
* @typedef {'amp' | 'claude' | 'cline' | 'codex' | 'cursor' | 'gemini' | 'kiro' | 'opencode' | 'kilo' | 'roo' | 'trae' | 'windsurf' | 'vscode' | 'zed'} RulesProfile
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -18,6 +18,7 @@
|
||||
* - gemini: Gemini integration
|
||||
* - kiro: Kiro IDE rules
|
||||
* - opencode: OpenCode integration
|
||||
* - kilo: Kilo Code integration
|
||||
* - roo: Roo Code IDE rules
|
||||
* - trae: Trae IDE rules
|
||||
* - vscode: VS Code with GitHub Copilot integration
|
||||
@@ -38,6 +39,7 @@ export const RULE_PROFILES = [
|
||||
'gemini',
|
||||
'kiro',
|
||||
'opencode',
|
||||
'kilo',
|
||||
'roo',
|
||||
'trae',
|
||||
'vscode',
|
||||
|
||||
@@ -5,6 +5,7 @@ export { clineProfile } from './cline.js';
|
||||
export { codexProfile } from './codex.js';
|
||||
export { cursorProfile } from './cursor.js';
|
||||
export { geminiProfile } from './gemini.js';
|
||||
export { kiloProfile } from './kilo.js';
|
||||
export { kiroProfile } from './kiro.js';
|
||||
export { opencodeProfile } from './opencode.js';
|
||||
export { rooProfile } from './roo.js';
|
||||
|
||||
186
src/profiles/kilo.js
Normal file
186
src/profiles/kilo.js
Normal file
@@ -0,0 +1,186 @@
|
||||
// Kilo Code conversion profile for rule-transformer
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { isSilentMode, log } from '../../scripts/modules/utils.js';
|
||||
import { createProfile, COMMON_TOOL_MAPPINGS } from './base-profile.js';
|
||||
import { ROO_MODES } from '../constants/profiles.js';
|
||||
|
||||
// Utility function to apply kilo transformations to content
|
||||
function applyKiloTransformations(content) {
|
||||
const customReplacements = [
|
||||
// Replace roo-specific terms with kilo equivalents
|
||||
{
|
||||
from: /\broo\b/gi,
|
||||
to: (match) => (match.charAt(0) === 'R' ? 'Kilo' : 'kilo')
|
||||
},
|
||||
{ from: /Roo/g, to: 'Kilo' },
|
||||
{ from: /ROO/g, to: 'KILO' },
|
||||
{ from: /roocode\.com/gi, to: 'kilocode.com' },
|
||||
{ from: /docs\.roocode\.com/gi, to: 'docs.kilocode.com' },
|
||||
{ from: /https?:\/\/roocode\.com/gi, to: 'https://kilocode.com' },
|
||||
{
|
||||
from: /https?:\/\/docs\.roocode\.com/gi,
|
||||
to: 'https://docs.kilocode.com'
|
||||
},
|
||||
{ from: /\.roo\//g, to: '.kilo/' },
|
||||
{ from: /\.roomodes/g, to: '.kilocodemodes' },
|
||||
// Handle file extensions and directory references
|
||||
{ from: /roo-rules/g, to: 'kilo-rules' },
|
||||
{ from: /rules-roo/g, to: 'rules-kilo' }
|
||||
];
|
||||
|
||||
let transformedContent = content;
|
||||
for (const replacement of customReplacements) {
|
||||
transformedContent = transformedContent.replace(
|
||||
replacement.from,
|
||||
replacement.to
|
||||
);
|
||||
}
|
||||
return transformedContent;
|
||||
}
|
||||
|
||||
// Utility function to copy files recursively
|
||||
function copyRecursiveSync(src, dest) {
|
||||
const exists = fs.existsSync(src);
|
||||
const stats = exists && fs.statSync(src);
|
||||
const isDirectory = exists && stats.isDirectory();
|
||||
if (isDirectory) {
|
||||
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
|
||||
fs.readdirSync(src).forEach((childItemName) => {
|
||||
copyRecursiveSync(
|
||||
path.join(src, childItemName),
|
||||
path.join(dest, childItemName)
|
||||
);
|
||||
});
|
||||
} else {
|
||||
fs.copyFileSync(src, dest);
|
||||
}
|
||||
}
|
||||
|
||||
// Lifecycle functions for Kilo profile
|
||||
function onAddRulesProfile(targetDir, assetsDir) {
|
||||
// Use the provided assets directory to find the roocode directory
|
||||
const sourceDir = path.join(assetsDir, 'roocode');
|
||||
|
||||
if (!fs.existsSync(sourceDir)) {
|
||||
log('error', `[Kilo] Source directory does not exist: ${sourceDir}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Copy basic roocode structure first
|
||||
copyRecursiveSync(sourceDir, targetDir);
|
||||
log('debug', `[Kilo] Copied roocode directory to ${targetDir}`);
|
||||
|
||||
// Transform .roomodes to .kilocodemodes
|
||||
const roomodesSrc = path.join(sourceDir, '.roomodes');
|
||||
const kilocodemodesDest = path.join(targetDir, '.kilocodemodes');
|
||||
if (fs.existsSync(roomodesSrc)) {
|
||||
try {
|
||||
const roomodesContent = fs.readFileSync(roomodesSrc, 'utf8');
|
||||
const transformedContent = applyKiloTransformations(roomodesContent);
|
||||
fs.writeFileSync(kilocodemodesDest, transformedContent);
|
||||
log('debug', `[Kilo] Created .kilocodemodes at ${kilocodemodesDest}`);
|
||||
|
||||
// Remove the original .roomodes file
|
||||
fs.unlinkSync(path.join(targetDir, '.roomodes'));
|
||||
} catch (err) {
|
||||
log('error', `[Kilo] Failed to transform .roomodes: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Transform .roo directory to .kilo and apply kilo transformations to mode-specific rules
|
||||
const rooModesDir = path.join(sourceDir, '.roo');
|
||||
const kiloModesDir = path.join(targetDir, '.kilo');
|
||||
|
||||
// Remove the copied .roo directory and create .kilo
|
||||
if (fs.existsSync(path.join(targetDir, '.roo'))) {
|
||||
fs.rmSync(path.join(targetDir, '.roo'), { recursive: true, force: true });
|
||||
}
|
||||
|
||||
for (const mode of ROO_MODES) {
|
||||
const src = path.join(rooModesDir, `rules-${mode}`, `${mode}-rules`);
|
||||
const dest = path.join(kiloModesDir, `rules-${mode}`, `${mode}-rules`);
|
||||
if (fs.existsSync(src)) {
|
||||
try {
|
||||
const destDir = path.dirname(dest);
|
||||
if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
|
||||
|
||||
// Read, transform, and write the rule file
|
||||
const ruleContent = fs.readFileSync(src, 'utf8');
|
||||
const transformedContent = applyKiloTransformations(ruleContent);
|
||||
fs.writeFileSync(dest, transformedContent);
|
||||
|
||||
log('debug', `[Kilo] Transformed and copied ${mode}-rules to ${dest}`);
|
||||
} catch (err) {
|
||||
log(
|
||||
'error',
|
||||
`[Kilo] Failed to transform ${src} to ${dest}: ${err.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onRemoveRulesProfile(targetDir) {
|
||||
const kilocodemodespath = path.join(targetDir, '.kilocodemodes');
|
||||
if (fs.existsSync(kilocodemodespath)) {
|
||||
try {
|
||||
fs.rmSync(kilocodemodespath, { force: true });
|
||||
log('debug', `[Kilo] Removed .kilocodemodes from ${kilocodemodespath}`);
|
||||
} catch (err) {
|
||||
log('error', `[Kilo] Failed to remove .kilocodemodes: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const kiloDir = path.join(targetDir, '.kilo');
|
||||
if (fs.existsSync(kiloDir)) {
|
||||
fs.readdirSync(kiloDir).forEach((entry) => {
|
||||
if (entry.startsWith('rules-')) {
|
||||
const modeDir = path.join(kiloDir, entry);
|
||||
try {
|
||||
fs.rmSync(modeDir, { recursive: true, force: true });
|
||||
log('debug', `[Kilo] Removed ${entry} directory from ${modeDir}`);
|
||||
} catch (err) {
|
||||
log('error', `[Kilo] Failed to remove ${modeDir}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (fs.readdirSync(kiloDir).length === 0) {
|
||||
try {
|
||||
fs.rmSync(kiloDir, { recursive: true, force: true });
|
||||
log('debug', `[Kilo] Removed empty .kilo directory from ${kiloDir}`);
|
||||
} catch (err) {
|
||||
log('error', `[Kilo] Failed to remove .kilo directory: ${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onPostConvertRulesProfile(targetDir, assetsDir) {
|
||||
onAddRulesProfile(targetDir, assetsDir);
|
||||
}
|
||||
|
||||
// Create and export kilo profile using the base factory with roo rule reuse
|
||||
export const kiloProfile = createProfile({
|
||||
name: 'kilo',
|
||||
displayName: 'Kilo Code',
|
||||
url: 'kilocode.com',
|
||||
docsUrl: 'docs.kilocode.com',
|
||||
profileDir: '.kilo',
|
||||
rulesDir: '.kilo/rules',
|
||||
toolMappings: COMMON_TOOL_MAPPINGS.ROO_STYLE,
|
||||
|
||||
fileMap: {
|
||||
// Map roo rule files to kilo equivalents
|
||||
'rules/cursor_rules.mdc': 'kilo_rules.md',
|
||||
'rules/dev_workflow.mdc': 'dev_workflow.md',
|
||||
'rules/self_improve.mdc': 'self_improve.md',
|
||||
'rules/taskmaster.mdc': 'taskmaster.md'
|
||||
},
|
||||
onAdd: onAddRulesProfile,
|
||||
onRemove: onRemoveRulesProfile,
|
||||
onPostConvert: onPostConvertRulesProfile
|
||||
});
|
||||
|
||||
// Export lifecycle functions separately to avoid naming conflicts
|
||||
export { onAddRulesProfile, onRemoveRulesProfile, onPostConvertRulesProfile };
|
||||
298
src/progress/base-progress-tracker.js
Normal file
298
src/progress/base-progress-tracker.js
Normal file
@@ -0,0 +1,298 @@
|
||||
import { newMultiBar } from './cli-progress-factory.js';
|
||||
|
||||
/**
|
||||
* Base class for progress trackers, handling common logic for time, tokens, estimation, and multibar management.
|
||||
*/
|
||||
export class BaseProgressTracker {
|
||||
constructor(options = {}) {
|
||||
this.numUnits = options.numUnits || 1;
|
||||
this.unitName = options.unitName || 'unit'; // e.g., 'task', 'subtask'
|
||||
this.startTime = null;
|
||||
this.completedUnits = 0;
|
||||
this.tokensIn = 0;
|
||||
this.tokensOut = 0;
|
||||
this.isEstimate = true; // For token display
|
||||
|
||||
// Time estimation properties
|
||||
this.bestAvgTimePerUnit = null;
|
||||
this.lastEstimateTime = null;
|
||||
this.lastEstimateSeconds = 0;
|
||||
|
||||
// UI components
|
||||
this.multibar = null;
|
||||
this.timeTokensBar = null;
|
||||
this.progressBar = null;
|
||||
this._timerInterval = null;
|
||||
|
||||
// State flags
|
||||
this.isStarted = false;
|
||||
this.isFinished = false;
|
||||
|
||||
// Allow subclasses to define custom properties
|
||||
this._initializeCustomProperties(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Protected method for subclasses to initialize custom properties.
|
||||
* @protected
|
||||
*/
|
||||
_initializeCustomProperties(options) {
|
||||
// Subclasses can override this
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the pluralized form of the unit name for safe property keys.
|
||||
* @returns {string} Pluralized unit name
|
||||
*/
|
||||
get unitNamePlural() {
|
||||
return `${this.unitName}s`;
|
||||
}
|
||||
|
||||
start() {
|
||||
if (this.isStarted || this.isFinished) return;
|
||||
|
||||
this.isStarted = true;
|
||||
this.startTime = Date.now();
|
||||
|
||||
this.multibar = newMultiBar();
|
||||
|
||||
// Create time/tokens bar using subclass-provided format
|
||||
this.timeTokensBar = this.multibar.create(
|
||||
1,
|
||||
0,
|
||||
{},
|
||||
{
|
||||
format: this._getTimeTokensBarFormat(),
|
||||
barsize: 1,
|
||||
hideCursor: true,
|
||||
clearOnComplete: false
|
||||
}
|
||||
);
|
||||
|
||||
// Create main progress bar using subclass-provided format
|
||||
this.progressBar = this.multibar.create(
|
||||
this.numUnits,
|
||||
0,
|
||||
{},
|
||||
{
|
||||
format: this._getProgressBarFormat(),
|
||||
barCompleteChar: '\u2588',
|
||||
barIncompleteChar: '\u2591'
|
||||
}
|
||||
);
|
||||
|
||||
this._updateTimeTokensBar();
|
||||
this.progressBar.update(0, { [this.unitNamePlural]: `0/${this.numUnits}` });
|
||||
|
||||
// Start timer
|
||||
this._timerInterval = setInterval(() => this._updateTimeTokensBar(), 1000);
|
||||
|
||||
// Allow subclasses to add custom bars or setup
|
||||
this._setupCustomUI();
|
||||
}
|
||||
|
||||
/**
|
||||
* Protected method for subclasses to add custom UI elements after start.
|
||||
* @protected
|
||||
*/
|
||||
_setupCustomUI() {
|
||||
// Subclasses can override this
|
||||
}
|
||||
|
||||
/**
|
||||
* Protected method to get the format for the time/tokens bar.
|
||||
* @protected
|
||||
* @returns {string} Format string for the time/tokens bar.
|
||||
*/
|
||||
_getTimeTokensBarFormat() {
|
||||
return `{clock} {elapsed} | Tokens (I/O): {in}/{out} | Est: {remaining}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Protected method to get the format for the main progress bar.
|
||||
* @protected
|
||||
* @returns {string} Format string for the progress bar.
|
||||
*/
|
||||
_getProgressBarFormat() {
|
||||
return `${this.unitName.charAt(0).toUpperCase() + this.unitName.slice(1)}s {${this.unitNamePlural}} |{bar}| {percentage}%`;
|
||||
}
|
||||
|
||||
updateTokens(tokensIn, tokensOut, isEstimate = false) {
|
||||
this.tokensIn = tokensIn || 0;
|
||||
this.tokensOut = tokensOut || 0;
|
||||
this.isEstimate = isEstimate;
|
||||
this._updateTimeTokensBar();
|
||||
}
|
||||
|
||||
_updateTimeTokensBar() {
|
||||
if (!this.timeTokensBar || this.isFinished) return;
|
||||
|
||||
const elapsed = this._formatElapsedTime();
|
||||
const remaining = this._estimateRemainingTime();
|
||||
const tokensLabel = this.isEstimate ? '~ Tokens (I/O)' : 'Tokens (I/O)';
|
||||
|
||||
this.timeTokensBar.update(1, {
|
||||
clock: '⏱️',
|
||||
elapsed,
|
||||
in: this.tokensIn,
|
||||
out: this.tokensOut,
|
||||
remaining,
|
||||
tokensLabel,
|
||||
// Subclasses can add more payload here via override
|
||||
...this._getCustomTimeTokensPayload()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Protected method for subclasses to provide custom payload for time/tokens bar.
|
||||
* @protected
|
||||
* @returns {Object} Custom payload object.
|
||||
*/
|
||||
_getCustomTimeTokensPayload() {
|
||||
return {};
|
||||
}
|
||||
|
||||
_formatElapsedTime() {
|
||||
if (!this.startTime) return '0m 00s';
|
||||
const seconds = Math.floor((Date.now() - this.startTime) / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
return `${minutes}m ${remainingSeconds.toString().padStart(2, '0')}s`;
|
||||
}
|
||||
|
||||
_estimateRemainingTime() {
|
||||
const progress = this._getProgressFraction();
|
||||
if (progress >= 1) return '~0s';
|
||||
|
||||
const now = Date.now();
|
||||
const elapsed = (now - this.startTime) / 1000;
|
||||
|
||||
if (progress === 0) return '~calculating...';
|
||||
|
||||
const avgTimePerUnit = elapsed / progress;
|
||||
|
||||
if (
|
||||
this.bestAvgTimePerUnit === null ||
|
||||
avgTimePerUnit < this.bestAvgTimePerUnit
|
||||
) {
|
||||
this.bestAvgTimePerUnit = avgTimePerUnit;
|
||||
}
|
||||
|
||||
const remainingUnits = this.numUnits * (1 - progress);
|
||||
let estimatedSeconds = Math.ceil(remainingUnits * this.bestAvgTimePerUnit);
|
||||
|
||||
// Stabilization logic
|
||||
if (this.lastEstimateTime) {
|
||||
const elapsedSinceEstimate = Math.floor(
|
||||
(now - this.lastEstimateTime) / 1000
|
||||
);
|
||||
const countdownSeconds = Math.max(
|
||||
0,
|
||||
this.lastEstimateSeconds - elapsedSinceEstimate
|
||||
);
|
||||
if (countdownSeconds === 0) return '~0s';
|
||||
estimatedSeconds = Math.min(estimatedSeconds, countdownSeconds);
|
||||
}
|
||||
|
||||
this.lastEstimateTime = now;
|
||||
this.lastEstimateSeconds = estimatedSeconds;
|
||||
|
||||
return `~${this._formatDuration(estimatedSeconds)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Protected method for subclasses to calculate current progress fraction (0-1).
|
||||
* Defaults to simple completedUnits / numUnits.
|
||||
* @protected
|
||||
* @returns {number} Progress fraction (can be fractional for subtasks).
|
||||
*/
|
||||
_getProgressFraction() {
|
||||
return this.completedUnits / this.numUnits;
|
||||
}
|
||||
|
||||
_formatDuration(seconds) {
|
||||
if (seconds < 60) return `${seconds}s`;
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
if (minutes < 60) {
|
||||
return remainingSeconds > 0
|
||||
? `${minutes}m ${remainingSeconds}s`
|
||||
: `${minutes}m`;
|
||||
}
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const remainingMinutes = minutes % 60;
|
||||
return `${hours}h ${remainingMinutes}m`;
|
||||
}
|
||||
|
||||
getElapsedTime() {
|
||||
return this.startTime ? Date.now() - this.startTime : 0;
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.isFinished) return;
|
||||
|
||||
this.isFinished = true;
|
||||
|
||||
if (this._timerInterval) {
|
||||
clearInterval(this._timerInterval);
|
||||
this._timerInterval = null;
|
||||
}
|
||||
|
||||
if (this.multibar) {
|
||||
this._updateTimeTokensBar();
|
||||
this.multibar.stop();
|
||||
}
|
||||
|
||||
// Ensure cleanup is called to prevent memory leaks
|
||||
this.cleanup();
|
||||
}
|
||||
|
||||
getSummary() {
|
||||
return {
|
||||
completedUnits: this.completedUnits,
|
||||
elapsedTime: this.getElapsedTime()
|
||||
// Subclasses should extend this
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup method to ensure proper resource disposal and prevent memory leaks.
|
||||
* Should be called when the progress tracker is no longer needed.
|
||||
*/
|
||||
cleanup() {
|
||||
// Stop any active timers
|
||||
if (this._timerInterval) {
|
||||
clearInterval(this._timerInterval);
|
||||
this._timerInterval = null;
|
||||
}
|
||||
|
||||
// Stop and clear multibar
|
||||
if (this.multibar) {
|
||||
try {
|
||||
this.multibar.stop();
|
||||
} catch (error) {
|
||||
// Ignore errors during cleanup
|
||||
}
|
||||
this.multibar = null;
|
||||
}
|
||||
|
||||
// Clear progress bar references
|
||||
this.timeTokensBar = null;
|
||||
this.progressBar = null;
|
||||
|
||||
// Reset state
|
||||
this.isStarted = false;
|
||||
this.isFinished = true;
|
||||
|
||||
// Allow subclasses to perform custom cleanup
|
||||
this._performCustomCleanup();
|
||||
}
|
||||
|
||||
/**
|
||||
* Protected method for subclasses to perform custom cleanup.
|
||||
* @protected
|
||||
*/
|
||||
_performCustomCleanup() {
|
||||
// Subclasses can override this
|
||||
}
|
||||
}
|
||||
115
src/progress/cli-progress-factory.js
Normal file
115
src/progress/cli-progress-factory.js
Normal file
@@ -0,0 +1,115 @@
|
||||
import cliProgress from 'cli-progress';
|
||||
|
||||
/**
|
||||
* Default configuration for progress bars
|
||||
* Extracted to avoid duplication and provide single source of truth
|
||||
*/
|
||||
const DEFAULT_CONFIG = {
|
||||
clearOnComplete: false,
|
||||
stopOnComplete: true,
|
||||
hideCursor: true,
|
||||
barsize: 40 // Standard terminal width for progress bar
|
||||
};
|
||||
|
||||
/**
|
||||
* Available presets for progress bar styling
|
||||
* Makes it easy to see what options are available
|
||||
*/
|
||||
const PRESETS = {
|
||||
shades_classic: cliProgress.Presets.shades_classic,
|
||||
shades_grey: cliProgress.Presets.shades_grey,
|
||||
rect: cliProgress.Presets.rect,
|
||||
legacy: cliProgress.Presets.legacy
|
||||
};
|
||||
|
||||
/**
|
||||
* Factory class for creating CLI progress bars
|
||||
* Provides a consistent interface for creating both single and multi-bar instances
|
||||
*/
|
||||
export class ProgressBarFactory {
|
||||
constructor(defaultOptions = {}, defaultPreset = PRESETS.shades_classic) {
|
||||
this.defaultOptions = { ...DEFAULT_CONFIG, ...defaultOptions };
|
||||
this.defaultPreset = defaultPreset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new single progress bar
|
||||
* @param {Object} opts - Custom options to override defaults
|
||||
* @param {Object} preset - Progress bar preset for styling
|
||||
* @returns {cliProgress.SingleBar} Configured single progress bar instance
|
||||
*/
|
||||
createSingleBar(opts = {}, preset = null) {
|
||||
const config = this._mergeConfig(opts);
|
||||
const barPreset = preset || this.defaultPreset;
|
||||
|
||||
return new cliProgress.SingleBar(config, barPreset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new multi-bar container
|
||||
* @param {Object} opts - Custom options to override defaults
|
||||
* @param {Object} preset - Progress bar preset for styling
|
||||
* @returns {cliProgress.MultiBar} Configured multi-bar instance
|
||||
*/
|
||||
createMultiBar(opts = {}, preset = null) {
|
||||
const config = this._mergeConfig(opts);
|
||||
const barPreset = preset || this.defaultPreset;
|
||||
|
||||
return new cliProgress.MultiBar(config, barPreset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges custom options with defaults
|
||||
* @private
|
||||
* @param {Object} customOpts - Custom options to merge
|
||||
* @returns {Object} Merged configuration
|
||||
*/
|
||||
_mergeConfig(customOpts) {
|
||||
return { ...this.defaultOptions, ...customOpts };
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the default configuration
|
||||
* @param {Object} options - New default options
|
||||
*/
|
||||
setDefaultOptions(options) {
|
||||
this.defaultOptions = { ...this.defaultOptions, ...options };
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the default preset
|
||||
* @param {Object} preset - New default preset
|
||||
*/
|
||||
setDefaultPreset(preset) {
|
||||
this.defaultPreset = preset;
|
||||
}
|
||||
}
|
||||
|
||||
// Create a default factory instance for backward compatibility
|
||||
const defaultFactory = new ProgressBarFactory();
|
||||
|
||||
/**
|
||||
* Legacy function for creating a single progress bar
|
||||
* @deprecated Use ProgressBarFactory.createSingleBar() instead
|
||||
* @param {Object} opts - Progress bar options
|
||||
* @returns {cliProgress.SingleBar} Single progress bar instance
|
||||
*/
|
||||
export function newSingle(opts = {}) {
|
||||
return defaultFactory.createSingleBar(opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy function for creating a multi-bar
|
||||
* @deprecated Use ProgressBarFactory.createMultiBar() instead
|
||||
* @param {Object} opts - Progress bar options
|
||||
* @returns {cliProgress.MultiBar} Multi-bar instance
|
||||
*/
|
||||
export function newMultiBar(opts = {}) {
|
||||
return defaultFactory.createMultiBar(opts);
|
||||
}
|
||||
|
||||
// Export presets for easy access
|
||||
export { PRESETS };
|
||||
|
||||
// Export the factory class as default
|
||||
export default ProgressBarFactory;
|
||||
221
src/progress/parse-prd-tracker.js
Normal file
221
src/progress/parse-prd-tracker.js
Normal file
@@ -0,0 +1,221 @@
|
||||
import chalk from 'chalk';
|
||||
import { newMultiBar } from './cli-progress-factory.js';
|
||||
import { BaseProgressTracker } from './base-progress-tracker.js';
|
||||
import {
|
||||
createProgressHeader,
|
||||
createProgressRow,
|
||||
createBorder
|
||||
} from './tracker-ui.js';
|
||||
import {
|
||||
getCliPriorityIndicators,
|
||||
getPriorityIndicator,
|
||||
getStatusBarPriorityIndicators,
|
||||
getPriorityColors
|
||||
} from '../ui/indicators.js';
|
||||
|
||||
// Get centralized priority indicators
|
||||
const PRIORITY_INDICATORS = getCliPriorityIndicators();
|
||||
const PRIORITY_DOTS = getStatusBarPriorityIndicators();
|
||||
const PRIORITY_COLORS = getPriorityColors();
|
||||
|
||||
// Constants
|
||||
const CONSTANTS = {
|
||||
DEBOUNCE_DELAY: 100,
|
||||
MAX_TITLE_LENGTH: 57,
|
||||
TRUNCATED_LENGTH: 54,
|
||||
TASK_ID_PAD_START: 3,
|
||||
TASK_ID_PAD_END: 4,
|
||||
PRIORITY_PAD_END: 3,
|
||||
VALID_PRIORITIES: ['high', 'medium', 'low'],
|
||||
DEFAULT_PRIORITY: 'medium'
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper class to manage update debouncing
|
||||
*/
|
||||
class UpdateDebouncer {
|
||||
constructor(delay = CONSTANTS.DEBOUNCE_DELAY) {
|
||||
this.delay = delay;
|
||||
this.pendingTimeout = null;
|
||||
}
|
||||
|
||||
debounce(callback) {
|
||||
this.clear();
|
||||
this.pendingTimeout = setTimeout(() => {
|
||||
callback();
|
||||
this.pendingTimeout = null;
|
||||
}, this.delay);
|
||||
}
|
||||
|
||||
clear() {
|
||||
if (this.pendingTimeout) {
|
||||
clearTimeout(this.pendingTimeout);
|
||||
this.pendingTimeout = null;
|
||||
}
|
||||
}
|
||||
|
||||
hasPending() {
|
||||
return this.pendingTimeout !== null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class to manage priority counts
|
||||
*/
|
||||
class PriorityManager {
|
||||
constructor() {
|
||||
this.priorities = { high: 0, medium: 0, low: 0 };
|
||||
}
|
||||
|
||||
increment(priority) {
|
||||
const normalized = this.normalize(priority);
|
||||
this.priorities[normalized]++;
|
||||
return normalized;
|
||||
}
|
||||
|
||||
normalize(priority) {
|
||||
const lowercased = priority
|
||||
? priority.toLowerCase()
|
||||
: CONSTANTS.DEFAULT_PRIORITY;
|
||||
return CONSTANTS.VALID_PRIORITIES.includes(lowercased)
|
||||
? lowercased
|
||||
: CONSTANTS.DEFAULT_PRIORITY;
|
||||
}
|
||||
|
||||
getCounts() {
|
||||
return { ...this.priorities };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class for formatting task display elements
|
||||
*/
|
||||
class TaskFormatter {
|
||||
static formatTitle(title, taskNumber) {
|
||||
if (!title) return `Task ${taskNumber}`;
|
||||
return title.length > CONSTANTS.MAX_TITLE_LENGTH
|
||||
? title.substring(0, CONSTANTS.TRUNCATED_LENGTH) + '...'
|
||||
: title;
|
||||
}
|
||||
|
||||
static formatPriority(priority) {
|
||||
return getPriorityIndicator(priority, false).padEnd(
|
||||
CONSTANTS.PRIORITY_PAD_END,
|
||||
' '
|
||||
);
|
||||
}
|
||||
|
||||
static formatTaskId(taskNumber) {
|
||||
return taskNumber
|
||||
.toString()
|
||||
.padStart(CONSTANTS.TASK_ID_PAD_START, ' ')
|
||||
.padEnd(CONSTANTS.TASK_ID_PAD_END, ' ');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks progress for PRD parsing operations with multibar display
|
||||
*/
|
||||
class ParsePrdTracker extends BaseProgressTracker {
|
||||
_initializeCustomProperties(options) {
|
||||
this.append = options.append;
|
||||
this.priorityManager = new PriorityManager();
|
||||
this.debouncer = new UpdateDebouncer();
|
||||
this.headerShown = false;
|
||||
}
|
||||
|
||||
_getTimeTokensBarFormat() {
|
||||
return `{clock} {elapsed} | ${PRIORITY_DOTS.high} {high} ${PRIORITY_DOTS.medium} {medium} ${PRIORITY_DOTS.low} {low} | Tokens (I/O): {in}/{out} | Est: {remaining}`;
|
||||
}
|
||||
|
||||
_getProgressBarFormat() {
|
||||
return 'Tasks {tasks} |{bar}| {percentage}%';
|
||||
}
|
||||
|
||||
_getCustomTimeTokensPayload() {
|
||||
return this.priorityManager.getCounts();
|
||||
}
|
||||
|
||||
addTaskLine(taskNumber, title, priority = 'medium') {
|
||||
if (!this.multibar || this.isFinished) return;
|
||||
|
||||
this._ensureHeaderShown();
|
||||
const normalizedPriority = this._updateTaskCounters(taskNumber, priority);
|
||||
|
||||
// Immediately update the time/tokens bar to show the new priority count
|
||||
this._updateTimeTokensBar();
|
||||
|
||||
this.debouncer.debounce(() => {
|
||||
this._updateProgressDisplay(taskNumber, title, normalizedPriority);
|
||||
});
|
||||
}
|
||||
|
||||
_ensureHeaderShown() {
|
||||
if (!this.headerShown) {
|
||||
this.headerShown = true;
|
||||
createProgressHeader(
|
||||
this.multibar,
|
||||
' TASK | PRI | TITLE',
|
||||
'------+-----+----------------------------------------------------------------'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_updateTaskCounters(taskNumber, priority) {
|
||||
const normalizedPriority = this.priorityManager.increment(priority);
|
||||
this.completedUnits = taskNumber;
|
||||
return normalizedPriority;
|
||||
}
|
||||
|
||||
_updateProgressDisplay(taskNumber, title, normalizedPriority) {
|
||||
this.progressBar.update(this.completedUnits, {
|
||||
tasks: `${this.completedUnits}/${this.numUnits}`
|
||||
});
|
||||
|
||||
const displayTitle = TaskFormatter.formatTitle(title, taskNumber);
|
||||
const priorityDisplay = TaskFormatter.formatPriority(normalizedPriority);
|
||||
const taskIdCentered = TaskFormatter.formatTaskId(taskNumber);
|
||||
|
||||
createProgressRow(
|
||||
this.multibar,
|
||||
` ${taskIdCentered} | ${priorityDisplay} | {title}`,
|
||||
{ title: displayTitle }
|
||||
);
|
||||
|
||||
createBorder(
|
||||
this.multibar,
|
||||
'------+-----+----------------------------------------------------------------'
|
||||
);
|
||||
|
||||
this._updateTimeTokensBar();
|
||||
}
|
||||
|
||||
finish() {
|
||||
// Flush any pending updates before finishing
|
||||
if (this.debouncer.hasPending()) {
|
||||
this.debouncer.clear();
|
||||
this._updateTimeTokensBar();
|
||||
}
|
||||
this.cleanup();
|
||||
super.finish();
|
||||
}
|
||||
|
||||
/**
|
||||
* Override cleanup to handle pending updates
|
||||
*/
|
||||
_performCustomCleanup() {
|
||||
this.debouncer.clear();
|
||||
}
|
||||
|
||||
getSummary() {
|
||||
return {
|
||||
...super.getSummary(),
|
||||
taskPriorities: this.priorityManager.getCounts(),
|
||||
actionVerb: this.append ? 'appended' : 'generated'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function createParsePrdTracker(options = {}) {
|
||||
return new ParsePrdTracker(options);
|
||||
}
|
||||
152
src/progress/progress-tracker-builder.js
Normal file
152
src/progress/progress-tracker-builder.js
Normal file
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* Configuration for progress tracker features
|
||||
*/
|
||||
class TrackerConfig {
|
||||
constructor() {
|
||||
this.features = new Set();
|
||||
this.spinnerFrames = null;
|
||||
this.unitName = 'unit';
|
||||
this.totalUnits = 100;
|
||||
}
|
||||
|
||||
addFeature(feature) {
|
||||
this.features.add(feature);
|
||||
}
|
||||
|
||||
hasFeature(feature) {
|
||||
return this.features.has(feature);
|
||||
}
|
||||
|
||||
getOptions() {
|
||||
return {
|
||||
numUnits: this.totalUnits,
|
||||
unitName: this.unitName,
|
||||
spinnerFrames: this.spinnerFrames,
|
||||
features: Array.from(this.features)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder for creating configured progress trackers
|
||||
*/
|
||||
export class ProgressTrackerBuilder {
|
||||
constructor() {
|
||||
this.config = new TrackerConfig();
|
||||
}
|
||||
|
||||
withPercent() {
|
||||
this.config.addFeature('percent');
|
||||
return this;
|
||||
}
|
||||
|
||||
withTokens() {
|
||||
this.config.addFeature('tokens');
|
||||
return this;
|
||||
}
|
||||
|
||||
withTasks() {
|
||||
this.config.addFeature('tasks');
|
||||
return this;
|
||||
}
|
||||
|
||||
withSpinner(messages) {
|
||||
if (!messages || !Array.isArray(messages)) {
|
||||
throw new Error('Spinner messages must be an array');
|
||||
}
|
||||
this.config.spinnerFrames = messages;
|
||||
return this;
|
||||
}
|
||||
|
||||
withUnits(total, unitName = 'unit') {
|
||||
this.config.totalUnits = total;
|
||||
this.config.unitName = unitName;
|
||||
return this;
|
||||
}
|
||||
|
||||
build() {
|
||||
return new ProgressTracker(this.config);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Base progress tracker with configurable features
|
||||
*/
|
||||
class ProgressTracker {
|
||||
constructor(config) {
|
||||
this.config = config;
|
||||
this.isActive = false;
|
||||
this.current = 0;
|
||||
this.spinnerIndex = 0;
|
||||
this.startTime = null;
|
||||
}
|
||||
|
||||
start() {
|
||||
this.isActive = true;
|
||||
this.startTime = Date.now();
|
||||
this.current = 0;
|
||||
|
||||
if (this.config.spinnerFrames) {
|
||||
this._startSpinner();
|
||||
}
|
||||
}
|
||||
|
||||
update(data = {}) {
|
||||
if (!this.isActive) return;
|
||||
|
||||
if (data.current !== undefined) {
|
||||
this.current = data.current;
|
||||
}
|
||||
|
||||
const progress = this._buildProgressData(data);
|
||||
return progress;
|
||||
}
|
||||
|
||||
finish() {
|
||||
this.isActive = false;
|
||||
|
||||
if (this.spinnerInterval) {
|
||||
clearInterval(this.spinnerInterval);
|
||||
this.spinnerInterval = null;
|
||||
}
|
||||
|
||||
return this._buildSummary();
|
||||
}
|
||||
|
||||
_startSpinner() {
|
||||
this.spinnerInterval = setInterval(() => {
|
||||
this.spinnerIndex =
|
||||
(this.spinnerIndex + 1) % this.config.spinnerFrames.length;
|
||||
}, 100);
|
||||
}
|
||||
|
||||
_buildProgressData(data) {
|
||||
const progress = { ...data };
|
||||
|
||||
if (this.config.hasFeature('percent')) {
|
||||
progress.percentage = Math.round(
|
||||
(this.current / this.config.totalUnits) * 100
|
||||
);
|
||||
}
|
||||
|
||||
if (this.config.hasFeature('tasks')) {
|
||||
progress.tasks = `${this.current}/${this.config.totalUnits}`;
|
||||
}
|
||||
|
||||
if (this.config.spinnerFrames) {
|
||||
progress.spinner = this.config.spinnerFrames[this.spinnerIndex];
|
||||
}
|
||||
|
||||
return progress;
|
||||
}
|
||||
|
||||
_buildSummary() {
|
||||
const elapsed = Date.now() - this.startTime;
|
||||
return {
|
||||
total: this.config.totalUnits,
|
||||
completed: this.current,
|
||||
elapsedMs: elapsed,
|
||||
features: Array.from(this.config.features)
|
||||
};
|
||||
}
|
||||
}
|
||||
159
src/progress/tracker-ui.js
Normal file
159
src/progress/tracker-ui.js
Normal file
@@ -0,0 +1,159 @@
|
||||
import chalk from 'chalk';
|
||||
|
||||
/**
|
||||
* Factory for creating progress bar elements
|
||||
*/
|
||||
class ProgressBarFactory {
|
||||
constructor(multibar) {
|
||||
if (!multibar) {
|
||||
throw new Error('Multibar instance is required');
|
||||
}
|
||||
this.multibar = multibar;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a progress bar with the given format
|
||||
*/
|
||||
createBar(format, payload = {}) {
|
||||
if (typeof format !== 'string') {
|
||||
throw new Error('Format must be a string');
|
||||
}
|
||||
|
||||
const bar = this.multibar.create(
|
||||
1, // total
|
||||
1, // current
|
||||
{},
|
||||
{
|
||||
format,
|
||||
barsize: 1,
|
||||
hideCursor: true,
|
||||
clearOnComplete: false
|
||||
}
|
||||
);
|
||||
|
||||
bar.update(1, payload);
|
||||
return bar;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a header with borders
|
||||
*/
|
||||
createHeader(headerFormat, borderFormat) {
|
||||
this.createBar(borderFormat); // Top border
|
||||
this.createBar(headerFormat); // Header
|
||||
this.createBar(borderFormat); // Bottom border
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a data row
|
||||
*/
|
||||
createRow(rowFormat, payload) {
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
throw new Error('Payload must be an object');
|
||||
}
|
||||
return this.createBar(rowFormat, payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a border element
|
||||
*/
|
||||
createBorder(borderFormat) {
|
||||
return this.createBar(borderFormat);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a bordered header for progress tables.
|
||||
* @param {Object} multibar - The multibar instance.
|
||||
* @param {string} headerFormat - Format string for the header row.
|
||||
* @param {string} borderFormat - Format string for the top and bottom borders.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function createProgressHeader(multibar, headerFormat, borderFormat) {
|
||||
const factory = new ProgressBarFactory(multibar);
|
||||
factory.createHeader(headerFormat, borderFormat);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a formatted data row for progress tables.
|
||||
* @param {Object} multibar - The multibar instance.
|
||||
* @param {string} rowFormat - Format string for the row.
|
||||
* @param {Object} payload - Data payload for the row format.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function createProgressRow(multibar, rowFormat, payload) {
|
||||
const factory = new ProgressBarFactory(multibar);
|
||||
factory.createRow(rowFormat, payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a border row for progress tables.
|
||||
* @param {Object} multibar - The multibar instance.
|
||||
* @param {string} borderFormat - Format string for the border.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function createBorder(multibar, borderFormat) {
|
||||
const factory = new ProgressBarFactory(multibar);
|
||||
factory.createBorder(borderFormat);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder for creating progress tables with consistent formatting
|
||||
*/
|
||||
export class ProgressTableBuilder {
|
||||
constructor(multibar) {
|
||||
this.factory = new ProgressBarFactory(multibar);
|
||||
this.borderStyle = '─';
|
||||
this.columnSeparator = '|';
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a formatted table header
|
||||
*/
|
||||
showHeader(columns = null) {
|
||||
// Default columns for task display
|
||||
const defaultColumns = [
|
||||
{ text: 'TASK', width: 6 },
|
||||
{ text: 'PRI', width: 5 },
|
||||
{ text: 'TITLE', width: 64 }
|
||||
];
|
||||
|
||||
const cols = columns || defaultColumns;
|
||||
const headerText = ' ' + cols.map((c) => c.text).join(' | ') + ' ';
|
||||
const borderLine = this.createBorderLine(cols.map((c) => c.width));
|
||||
|
||||
this.factory.createHeader(headerText, borderLine);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a border line based on column widths
|
||||
*/
|
||||
createBorderLine(columnWidths) {
|
||||
return columnWidths
|
||||
.map((width) => this.borderStyle.repeat(width))
|
||||
.join('─┼─');
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a task row to the table
|
||||
*/
|
||||
addTaskRow(taskId, priority, title) {
|
||||
const format = ` ${taskId} | ${priority} | {title}`;
|
||||
this.factory.createRow(format, { title });
|
||||
|
||||
// Add separator after each row
|
||||
const borderLine = '------+-----+' + '─'.repeat(64);
|
||||
this.factory.createBorder(borderLine);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a summary row
|
||||
*/
|
||||
addSummaryRow(label, value) {
|
||||
const format = ` ${label}: {value}`;
|
||||
this.factory.createRow(format, { value });
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -45,12 +45,24 @@
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Use research mode"
|
||||
},
|
||||
"hasCodebaseAnalysis": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": false,
|
||||
"description": "Whether codebase analysis is available"
|
||||
},
|
||||
"projectRoot": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "",
|
||||
"description": "Project root path for context"
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
"default": {
|
||||
"system": "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\nWhen determining dependencies for a new task, follow these principles:\n1. Select dependencies based on logical requirements - what must be completed before this task can begin.\n2. Prioritize task dependencies that are semantically related to the functionality being built.\n3. Consider both direct dependencies (immediately prerequisite) and indirect dependencies.\n4. Avoid adding unnecessary dependencies - only include tasks that are genuinely prerequisite.\n5. Consider the current status of tasks - prefer completed tasks as dependencies when possible.\n6. Pay special attention to foundation tasks (1-5) but don't automatically include them without reason.\n7. Recent tasks (higher ID numbers) may be more relevant for newer functionality.\n\nThe dependencies array should contain task IDs (numbers) of prerequisite tasks.{{#if useResearch}}\n\nResearch current best practices and technologies relevant to this task.{{/if}}",
|
||||
"user": "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.\n \n {{gatheredContext}}\n \n {{#if useResearch}}Research current best practices, technologies, and implementation patterns relevant to this task. {{/if}}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.\n \n Return your answer as a single JSON object matching the schema precisely:\n \n {\n \"title\": \"Task title goes here\",\n \"description\": \"A concise one or two sentence description of what the task involves\",\n \"details\": \"Detailed implementation steps, considerations, code examples, or technical approach\",\n \"testStrategy\": \"Specific steps to verify correct implementation and functionality\",\n \"dependencies\": [1, 3] // Example: IDs of tasks that must be completed before this task\n }\n \n Make sure the details and test strategy are comprehensive and specific{{#if useResearch}}, incorporating current best practices from your research{{/if}}. DO NOT include the task ID in the title.\n {{#if contextFromArgs}}{{contextFromArgs}}{{/if}}"
|
||||
"user": "{{#if hasCodebaseAnalysis}}## IMPORTANT: Codebase Analysis Required\n\nYou have access to powerful codebase analysis tools. Before generating the task:\n\n1. Use the Glob tool to explore the project structure (e.g., \"**/*.js\", \"**/*.json\", \"**/README.md\")\n2. Use the Grep tool to search for existing implementations, patterns, and technologies\n3. Use the Read tool to examine key files like package.json, main entry points, and relevant source files\n4. Analyze the current implementation to understand what already exists\n\nBased on your analysis:\n- Identify existing components/features that relate to this new task\n- Understand the technology stack, frameworks, and patterns in use\n- Generate implementation details that align with the project's current architecture\n- Reference specific files, functions, or patterns from the codebase in your details\n\nProject Root: {{projectRoot}}\n\n{{/if}}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.\n \n {{gatheredContext}}\n \n {{#if useResearch}}Research current best practices, technologies, and implementation patterns relevant to this task. {{/if}}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.\n \n Return your answer as a single JSON object matching the schema precisely:\n \n {\n \"title\": \"Task title goes here\",\n \"description\": \"A concise one or two sentence description of what the task involves\",\n \"details\": \"Detailed implementation steps, considerations, code examples, or technical approach\",\n \"testStrategy\": \"Specific steps to verify correct implementation and functionality\",\n \"dependencies\": [1, 3] // Example: IDs of tasks that must be completed before this task\n }\n \n Make sure the details and test strategy are comprehensive and specific{{#if useResearch}}, incorporating current best practices from your research{{/if}}. DO NOT include the task ID in the title.\n {{#if contextFromArgs}}{{contextFromArgs}}{{/if}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,10 +31,10 @@
|
||||
"default": false,
|
||||
"description": "Use research mode for deeper analysis"
|
||||
},
|
||||
"isClaudeCode": {
|
||||
"hasCodebaseAnalysis": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Whether Claude Code is being used as the provider"
|
||||
"description": "Whether codebase analysis is available"
|
||||
},
|
||||
"projectRoot": {
|
||||
"type": "string",
|
||||
@@ -45,7 +45,7 @@
|
||||
"prompts": {
|
||||
"default": {
|
||||
"system": "You are an expert software architect and project manager analyzing task complexity. Respond only with the requested valid JSON array.",
|
||||
"user": "{{#if isClaudeCode}}## IMPORTANT: Codebase Analysis Required\n\nYou have access to powerful codebase analysis tools. Before analyzing task complexity:\n\n1. Use the Glob tool to explore the project structure and understand the codebase size\n2. Use the Grep tool to search for existing implementations related to each task\n3. Use the Read tool to examine key files that would be affected by these tasks\n4. Understand the current implementation state, patterns used, and technical debt\n\nBased on your codebase analysis:\n- Assess complexity based on ACTUAL code that needs to be modified/created\n- Consider existing abstractions and patterns that could simplify implementation\n- Identify tasks that require refactoring vs. greenfield development\n- Factor in dependencies between existing code and new features\n- Provide more accurate subtask recommendations based on real code structure\n\nProject Root: {{projectRoot}}\n\n{{/if}}Analyze the following tasks to determine their complexity (1-10 scale) and recommend the number of subtasks for expansion. Provide a brief reasoning and an initial expansion prompt for each.{{#if useResearch}} Consider current best practices, common implementation patterns, and industry standards in your analysis.{{/if}}\n\nTasks:\n{{{json tasks}}}\n{{#if gatheredContext}}\n\n# Project Context\n\n{{gatheredContext}}\n{{/if}}\n\nRespond ONLY with a valid JSON array matching the schema:\n[\n {\n \"taskId\": <number>,\n \"taskTitle\": \"<string>\",\n \"complexityScore\": <number 1-10>,\n \"recommendedSubtasks\": <number>,\n \"expansionPrompt\": \"<string>\",\n \"reasoning\": \"<string>\"\n },\n ...\n]\n\nDo not include any explanatory text, markdown formatting, or code block markers before or after the JSON array."
|
||||
"user": "{{#if hasCodebaseAnalysis}}## IMPORTANT: Codebase Analysis Required\n\nYou have access to powerful codebase analysis tools. Before analyzing task complexity:\n\n1. Use the Glob tool to explore the project structure and understand the codebase size\n2. Use the Grep tool to search for existing implementations related to each task\n3. Use the Read tool to examine key files that would be affected by these tasks\n4. Understand the current implementation state, patterns used, and technical debt\n\nBased on your codebase analysis:\n- Assess complexity based on ACTUAL code that needs to be modified/created\n- Consider existing abstractions and patterns that could simplify implementation\n- Identify tasks that require refactoring vs. greenfield development\n- Factor in dependencies between existing code and new features\n- Provide more accurate subtask recommendations based on real code structure\n\nProject Root: {{projectRoot}}\n\n{{/if}}Analyze the following tasks to determine their complexity (1-10 scale) and recommend the number of subtasks for expansion. Provide a brief reasoning and an initial expansion prompt for each.{{#if useResearch}} Consider current best practices, common implementation patterns, and industry standards in your analysis.{{/if}}\n\nTasks:\n{{{json tasks}}}\n{{#if gatheredContext}}\n\n# Project Context\n\n{{gatheredContext}}\n{{/if}}\n\nRespond ONLY with a valid JSON array matching the schema:\n[\n {\n \"taskId\": <number>,\n \"taskTitle\": \"<string>\",\n \"complexityScore\": <number 1-10>,\n \"recommendedSubtasks\": <number>,\n \"expansionPrompt\": \"<string>\",\n \"reasoning\": \"<string>\"\n },\n ...\n]\n\nDo not include any explanatory text, markdown formatting, or code block markers before or after the JSON array."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,11 +52,11 @@
|
||||
"default": "",
|
||||
"description": "Gathered project context"
|
||||
},
|
||||
"isClaudeCode": {
|
||||
"hasCodebaseAnalysis": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": false,
|
||||
"description": "Whether Claude Code is being used as the provider"
|
||||
"description": "Whether codebase analysis is available"
|
||||
},
|
||||
"projectRoot": {
|
||||
"type": "string",
|
||||
@@ -74,11 +74,11 @@
|
||||
"research": {
|
||||
"condition": "useResearch === true && !expansionPrompt",
|
||||
"system": "You are an AI assistant that responds ONLY with valid JSON objects as requested. The object should contain a 'subtasks' array.",
|
||||
"user": "{{#if isClaudeCode}}## IMPORTANT: Codebase Analysis Required\n\nYou have access to powerful codebase analysis tools. Before generating subtasks:\n\n1. Use the Glob tool to explore relevant files for this task (e.g., \"**/*.js\", \"src/**/*.ts\")\n2. Use the Grep tool to search for existing implementations related to this task\n3. Use the Read tool to examine files that would be affected by this task\n4. Understand the current implementation state and patterns used\n\nBased on your analysis:\n- Identify existing code that relates to this task\n- Understand patterns and conventions to follow\n- Generate subtasks that integrate smoothly with existing code\n- Ensure subtasks are specific and actionable based on the actual codebase\n\nProject Root: {{projectRoot}}\n\n{{/if}}Analyze the following task and break it down into {{#if (gt subtaskCount 0)}}exactly {{subtaskCount}}{{else}}an appropriate number of{{/if}} specific subtasks using your research capabilities. Assign sequential IDs starting from {{nextSubtaskId}}.\n\nParent Task:\nID: {{task.id}}\nTitle: {{task.title}}\nDescription: {{task.description}}\nCurrent details: {{#if task.details}}{{task.details}}{{else}}None{{/if}}{{#if additionalContext}}\nConsider this context: {{additionalContext}}{{/if}}{{#if complexityReasoningContext}}\nComplexity Analysis Reasoning: {{complexityReasoningContext}}{{/if}}{{#if gatheredContext}}\n\n# Project Context\n\n{{gatheredContext}}{{/if}}\n\nCRITICAL: Respond ONLY with a valid JSON object containing a single key \"subtasks\". The value must be an array of the generated subtasks, strictly matching this structure:\n\n{\n \"subtasks\": [\n {\n \"id\": <number>, // Sequential ID starting from {{nextSubtaskId}}\n \"title\": \"<string>\",\n \"description\": \"<string>\",\n \"dependencies\": [\"<string>\"], // Use full subtask IDs like [\"{{task.id}}.1\", \"{{task.id}}.2\"]. If no dependencies, use an empty array [].\n \"details\": \"<string>\",\n \"testStrategy\": \"<string>\" // Optional\n },\n // ... (repeat for {{#if (gt subtaskCount 0)}}{{subtaskCount}}{{else}}appropriate number of{{/if}} subtasks)\n ]\n}\n\nImportant: For the 'dependencies' field, if a subtask has no dependencies, you MUST use an empty array, for example: \"dependencies\": []. Do not use null or omit the field.\n\nDo not include ANY explanatory text, markdown, or code block markers. Just the JSON object."
|
||||
"user": "{{#if hasCodebaseAnalysis}}## IMPORTANT: Codebase Analysis Required\n\nYou have access to powerful codebase analysis tools. Before generating subtasks:\n\n1. Use the Glob tool to explore relevant files for this task (e.g., \"**/*.js\", \"src/**/*.ts\")\n2. Use the Grep tool to search for existing implementations related to this task\n3. Use the Read tool to examine files that would be affected by this task\n4. Understand the current implementation state and patterns used\n\nBased on your analysis:\n- Identify existing code that relates to this task\n- Understand patterns and conventions to follow\n- Generate subtasks that integrate smoothly with existing code\n- Ensure subtasks are specific and actionable based on the actual codebase\n\nProject Root: {{projectRoot}}\n\n{{/if}}Analyze the following task and break it down into {{#if (gt subtaskCount 0)}}exactly {{subtaskCount}}{{else}}an appropriate number of{{/if}} specific subtasks using your research capabilities. Assign sequential IDs starting from {{nextSubtaskId}}.\n\nParent Task:\nID: {{task.id}}\nTitle: {{task.title}}\nDescription: {{task.description}}\nCurrent details: {{#if task.details}}{{task.details}}{{else}}None{{/if}}{{#if additionalContext}}\nConsider this context: {{additionalContext}}{{/if}}{{#if complexityReasoningContext}}\nComplexity Analysis Reasoning: {{complexityReasoningContext}}{{/if}}{{#if gatheredContext}}\n\n# Project Context\n\n{{gatheredContext}}{{/if}}\n\nCRITICAL: Respond ONLY with a valid JSON object containing a single key \"subtasks\". The value must be an array of the generated subtasks, strictly matching this structure:\n\n{\n \"subtasks\": [\n {\n \"id\": <number>, // Sequential ID starting from {{nextSubtaskId}}\n \"title\": \"<string>\",\n \"description\": \"<string>\",\n \"dependencies\": [\"<string>\"], // Use full subtask IDs like [\"{{task.id}}.1\", \"{{task.id}}.2\"]. If no dependencies, use an empty array [].\n \"details\": \"<string>\",\n \"testStrategy\": \"<string>\" // Optional\n },\n // ... (repeat for {{#if (gt subtaskCount 0)}}{{subtaskCount}}{{else}}appropriate number of{{/if}} subtasks)\n ]\n}\n\nImportant: For the 'dependencies' field, if a subtask has no dependencies, you MUST use an empty array, for example: \"dependencies\": []. Do not use null or omit the field.\n\nDo not include ANY explanatory text, markdown, or code block markers. Just the JSON object."
|
||||
},
|
||||
"default": {
|
||||
"system": "You are an AI assistant helping with task breakdown for software development.\nYou need to break down a high-level task into {{#if (gt subtaskCount 0)}}{{subtaskCount}}{{else}}an appropriate number of{{/if}} specific subtasks that can be implemented one by one.\n\nSubtasks should:\n1. Be specific and actionable implementation steps\n2. Follow a logical sequence\n3. Each handle a distinct part of the parent task\n4. Include clear guidance on implementation approach\n5. Have appropriate dependency chains between subtasks (using full subtask IDs)\n6. Collectively cover all aspects of the parent task\n\nFor each subtask, provide:\n- id: Sequential integer starting from the provided nextSubtaskId\n- title: Clear, specific title\n- description: Detailed description\n- dependencies: Array of prerequisite subtask IDs using full format like [\"{{task.id}}.1\", \"{{task.id}}.2\"]\n- details: Implementation details, the output should be in string\n- testStrategy: Optional testing approach\n\nRespond ONLY with a valid JSON object containing a single key \"subtasks\" whose value is an array matching the structure described. Do not include any explanatory text, markdown formatting, or code block markers.",
|
||||
"user": "{{#if isClaudeCode}}## IMPORTANT: Codebase Analysis Required\n\nYou have access to powerful codebase analysis tools. Before generating subtasks:\n\n1. Use the Glob tool to explore relevant files for this task (e.g., \"**/*.js\", \"src/**/*.ts\")\n2. Use the Grep tool to search for existing implementations related to this task\n3. Use the Read tool to examine files that would be affected by this task\n4. Understand the current implementation state and patterns used\n\nBased on your analysis:\n- Identify existing code that relates to this task\n- Understand patterns and conventions to follow\n- Generate subtasks that integrate smoothly with existing code\n- Ensure subtasks are specific and actionable based on the actual codebase\n\nProject Root: {{projectRoot}}\n\n{{/if}}Break down this task into {{#if (gt subtaskCount 0)}}exactly {{subtaskCount}}{{else}}an appropriate number of{{/if}} specific subtasks:\n\nTask ID: {{task.id}}\nTitle: {{task.title}}\nDescription: {{task.description}}\nCurrent details: {{#if task.details}}{{task.details}}{{else}}None{{/if}}{{#if additionalContext}}\nAdditional context: {{additionalContext}}{{/if}}{{#if complexityReasoningContext}}\nComplexity Analysis Reasoning: {{complexityReasoningContext}}{{/if}}{{#if gatheredContext}}\n\n# Project Context\n\n{{gatheredContext}}{{/if}}\n\nReturn ONLY the JSON object containing the \"subtasks\" array, matching this structure:\n\n{\n \"subtasks\": [\n {\n \"id\": {{nextSubtaskId}}, // First subtask ID\n \"title\": \"Specific subtask title\",\n \"description\": \"Detailed description\",\n \"dependencies\": [], // e.g., [\"{{task.id}}.1\", \"{{task.id}}.2\"] for dependencies. Use empty array [] if no dependencies\n \"details\": \"Implementation guidance\",\n \"testStrategy\": \"Optional testing approach\"\n },\n // ... (repeat for {{#if (gt subtaskCount 0)}}a total of {{subtaskCount}}{{else}}an appropriate number of{{/if}} subtasks with sequential IDs)\n ]\n}"
|
||||
"user": "{{#if hasCodebaseAnalysis}}## IMPORTANT: Codebase Analysis Required\n\nYou have access to powerful codebase analysis tools. Before generating subtasks:\n\n1. Use the Glob tool to explore relevant files for this task (e.g., \"**/*.js\", \"src/**/*.ts\")\n2. Use the Grep tool to search for existing implementations related to this task\n3. Use the Read tool to examine files that would be affected by this task\n4. Understand the current implementation state and patterns used\n\nBased on your analysis:\n- Identify existing code that relates to this task\n- Understand patterns and conventions to follow\n- Generate subtasks that integrate smoothly with existing code\n- Ensure subtasks are specific and actionable based on the actual codebase\n\nProject Root: {{projectRoot}}\n\n{{/if}}Break down this task into {{#if (gt subtaskCount 0)}}exactly {{subtaskCount}}{{else}}an appropriate number of{{/if}} specific subtasks:\n\nTask ID: {{task.id}}\nTitle: {{task.title}}\nDescription: {{task.description}}\nCurrent details: {{#if task.details}}{{task.details}}{{else}}None{{/if}}{{#if additionalContext}}\nAdditional context: {{additionalContext}}{{/if}}{{#if complexityReasoningContext}}\nComplexity Analysis Reasoning: {{complexityReasoningContext}}{{/if}}{{#if gatheredContext}}\n\n# Project Context\n\n{{gatheredContext}}{{/if}}\n\nReturn ONLY the JSON object containing the \"subtasks\" array, matching this structure:\n\n{\n \"subtasks\": [\n {\n \"id\": {{nextSubtaskId}}, // First subtask ID\n \"title\": \"Specific subtask title\",\n \"description\": \"Detailed description\",\n \"dependencies\": [], // e.g., [\"{{task.id}}.1\", \"{{task.id}}.2\"] for dependencies. Use empty array [] if no dependencies\n \"details\": \"Implementation guidance\",\n \"testStrategy\": \"Optional testing approach\"\n },\n // ... (repeat for {{#if (gt subtaskCount 0)}}a total of {{subtaskCount}}{{else}}an appropriate number of{{/if}} subtasks with sequential IDs)\n ]\n}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,11 +41,11 @@
|
||||
"enum": ["high", "medium", "low"],
|
||||
"description": "Default priority for generated tasks"
|
||||
},
|
||||
"isClaudeCode": {
|
||||
"hasCodebaseAnalysis": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": false,
|
||||
"description": "Whether Claude Code is being used as the provider"
|
||||
"description": "Whether codebase analysis is available"
|
||||
},
|
||||
"projectRoot": {
|
||||
"type": "string",
|
||||
@@ -57,7 +57,7 @@
|
||||
"prompts": {
|
||||
"default": {
|
||||
"system": "You are an AI assistant specialized in analyzing Product Requirements Documents (PRDs) and generating a structured, logically ordered, dependency-aware and sequenced list of development tasks in JSON format.{{#if research}}\nBefore breaking down the PRD into tasks, you will:\n1. Research and analyze the latest technologies, libraries, frameworks, and best practices that would be appropriate for this project\n2. Identify any potential technical challenges, security concerns, or scalability issues not explicitly mentioned in the PRD without discarding any explicit requirements or going overboard with complexity -- always aim to provide the most direct path to implementation, avoiding over-engineering or roundabout approaches\n3. Consider current industry standards and evolving trends relevant to this project (this step aims to solve LLM hallucinations and out of date information due to training data cutoff dates)\n4. Evaluate alternative implementation approaches and recommend the most efficient path\n5. Include specific library versions, helpful APIs, and concrete implementation guidance based on your research\n6. Always aim to provide the most direct path to implementation, avoiding over-engineering or roundabout approaches\n\nYour task breakdown should incorporate this research, resulting in more detailed implementation guidance, more accurate dependency mapping, and more precise technology recommendations than would be possible from the PRD text alone, while maintaining all explicit requirements and best practices and all details and nuances of the PRD.{{/if}}\n\nAnalyze the provided PRD content and generate {{#if (gt numTasks 0)}}approximately {{numTasks}}{{else}}an appropriate number of{{/if}} top-level development tasks. If the complexity or the level of detail of the PRD is high, generate more tasks relative to the complexity of the PRD\nEach task should represent a logical unit of work needed to implement the requirements and focus on the most direct and effective way to implement the requirements without unnecessary complexity or overengineering. Include pseudo-code, implementation details, and test strategy for each task. Find the most up to date information to implement each task.\nAssign sequential IDs starting from {{nextId}}. Infer title, description, details, and test strategy for each task based *only* on the PRD content.\nSet status to 'pending', dependencies to an empty array [], and priority to '{{defaultTaskPriority}}' initially for all tasks.\nRespond ONLY with a valid JSON object containing a single key \"tasks\", where the value is an array of task objects adhering to the provided Zod schema. Do not include any explanation or markdown formatting.\n\nEach task should follow this JSON structure:\n{\n\t\"id\": number,\n\t\"title\": string,\n\t\"description\": string,\n\t\"status\": \"pending\",\n\t\"dependencies\": number[] (IDs of tasks this depends on),\n\t\"priority\": \"high\" | \"medium\" | \"low\",\n\t\"details\": string (implementation details),\n\t\"testStrategy\": string (validation approach)\n}\n\nGuidelines:\n1. {{#if (gt numTasks 0)}}Unless complexity warrants otherwise{{else}}Depending on the complexity{{/if}}, create {{#if (gt numTasks 0)}}exactly {{numTasks}}{{else}}an appropriate number of{{/if}} tasks, numbered sequentially starting from {{nextId}}\n2. Each task should be atomic and focused on a single responsibility following the most up to date best practices and standards\n3. Order tasks logically - consider dependencies and implementation sequence\n4. Early tasks should focus on setup, core functionality first, then advanced features\n5. Include clear validation/testing approach for each task\n6. Set appropriate dependency IDs (a task can only depend on tasks with lower IDs, potentially including existing tasks with IDs less than {{nextId}} if applicable)\n7. Assign priority (high/medium/low) based on criticality and dependency order\n8. Include detailed implementation guidance in the \"details\" field{{#if research}}, with specific libraries and version recommendations based on your research{{/if}}\n9. If the PRD contains specific requirements for libraries, database schemas, frameworks, tech stacks, or any other implementation details, STRICTLY ADHERE to these requirements in your task breakdown and do not discard them under any circumstance\n10. Focus on filling in any gaps left by the PRD or areas that aren't fully specified, while preserving all explicit requirements\n11. Always aim to provide the most direct path to implementation, avoiding over-engineering or roundabout approaches{{#if research}}\n12. For each task, include specific, actionable guidance based on current industry standards and best practices discovered through research{{/if}}",
|
||||
"user": "{{#if isClaudeCode}}## IMPORTANT: Codebase Analysis Required\n\nYou have access to powerful codebase analysis tools. Before generating tasks:\n\n1. Use the Glob tool to explore the project structure (e.g., \"**/*.js\", \"**/*.json\", \"**/README.md\")\n2. Use the Grep tool to search for existing implementations, patterns, and technologies\n3. Use the Read tool to examine key files like package.json, README.md, and main entry points\n4. Analyze the current state of implementation to understand what already exists\n\nBased on your analysis:\n- Identify what components/features are already implemented\n- Understand the technology stack, frameworks, and patterns in use\n- Generate tasks that build upon the existing codebase rather than duplicating work\n- Ensure tasks align with the project's current architecture and conventions\n\nProject Root: {{projectRoot}}\n\n{{/if}}Here's the Product Requirements Document (PRD) to break down into {{#if (gt numTasks 0)}}approximately {{numTasks}}{{else}}an appropriate number of{{/if}} tasks, starting IDs from {{nextId}}:{{#if research}}\n\nRemember to thoroughly research current best practices and technologies before task breakdown to provide specific, actionable implementation details.{{/if}}\n\n{{prdContent}}\n\n\n\t\tReturn your response in this format:\n{\n \"tasks\": [\n {\n \"id\": 1,\n \"title\": \"Setup Project Repository\",\n \"description\": \"...\",\n ...\n },\n ...\n ],\n \"metadata\": {\n \"projectName\": \"PRD Implementation\",\n \"totalTasks\": {{#if (gt numTasks 0)}}{{numTasks}}{{else}}{number of tasks}{{/if}},\n \"sourceFile\": \"{{prdPath}}\",\n \"generatedAt\": \"YYYY-MM-DD\"\n }\n}"
|
||||
"user": "{{#if hasCodebaseAnalysis}}## IMPORTANT: Codebase Analysis Required\n\nYou have access to powerful codebase analysis tools. Before generating tasks:\n\n1. Use the Glob tool to explore the project structure (e.g., \"**/*.js\", \"**/*.json\", \"**/README.md\")\n2. Use the Grep tool to search for existing implementations, patterns, and technologies\n3. Use the Read tool to examine key files like package.json, README.md, and main entry points\n4. Analyze the current state of implementation to understand what already exists\n\nBased on your analysis:\n- Identify what components/features are already implemented\n- Understand the technology stack, frameworks, and patterns in use\n- Generate tasks that build upon the existing codebase rather than duplicating work\n- Ensure tasks align with the project's current architecture and conventions\n\nProject Root: {{projectRoot}}\n\n{{/if}}Here's the Product Requirements Document (PRD) to break down into {{#if (gt numTasks 0)}}approximately {{numTasks}}{{else}}an appropriate number of{{/if}} tasks, starting IDs from {{nextId}}:{{#if research}}\n\nRemember to thoroughly research current best practices and technologies before task breakdown to provide specific, actionable implementation details.{{/if}}\n\n{{prdContent}}\n\n\n\t\tReturn your response in this format:\n{\n \"tasks\": [\n {\n \"id\": 1,\n \"title\": \"Setup Project Repository\",\n \"description\": \"...\",\n ...\n },\n ...\n ],\n \"metadata\": {\n \"projectName\": \"PRD Implementation\",\n \"totalTasks\": {{#if (gt numTasks 0)}}{{numTasks}}{{else}}{number of tasks}{{/if}},\n \"sourceFile\": \"{{prdPath}}\",\n \"generatedAt\": \"YYYY-MM-DD\"\n }\n}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,12 +44,24 @@
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Additional project context"
|
||||
},
|
||||
"hasCodebaseAnalysis": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": false,
|
||||
"description": "Whether codebase analysis is available"
|
||||
},
|
||||
"projectRoot": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "",
|
||||
"description": "Project root path for context"
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
"default": {
|
||||
"system": "You are an AI assistant helping to update a subtask. You will be provided with the subtask's existing details, context about its parent and sibling tasks, and a user request string.{{#if useResearch}} You have access to current best practices and latest technical information to provide research-backed updates.{{/if}}\n\nYour Goal: Based *only* on the user's request and all the provided context (including existing details if relevant to the request), GENERATE the new text content that should be added to the subtask's details.\nFocus *only* on generating the substance of the update.\n\nOutput Requirements:\n1. Return *only* the newly generated text content as a plain string. Do NOT return a JSON object or any other structured data.\n2. Your string response should NOT include any of the subtask's original details, unless the user's request explicitly asks to rephrase, summarize, or directly modify existing text.\n3. Do NOT include any timestamps, XML-like tags, markdown, or any other special formatting in your string response.\n4. Ensure the generated text is concise yet complete for the update based on the user request. Avoid conversational fillers or explanations about what you are doing (e.g., do not start with \"Okay, here's the update...\").{{#if useResearch}}\n5. Include specific libraries, versions, and current best practices relevant to the subtask implementation.\n6. Provide research-backed technical recommendations and proven approaches.{{/if}}",
|
||||
"user": "Task Context:\n\nParent Task: {{{json parentTask}}}\n{{#if prevSubtask}}Previous Subtask: {{{json prevSubtask}}}\n{{/if}}{{#if nextSubtask}}Next Subtask: {{{json nextSubtask}}}\n{{/if}}Current Subtask Details (for context only):\n{{currentDetails}}\n\nUser Request: \"{{updatePrompt}}\"\n\n{{#if useResearch}}Research and incorporate current best practices, latest stable versions, and proven approaches into your update. {{/if}}Based on the User Request and all the Task Context (including current subtask details provided above), what is the new information or text that should be appended to this subtask's details? Return ONLY this new text as a plain string.{{#if useResearch}} Include specific technical recommendations based on current industry standards.{{/if}}\n{{#if gatheredContext}}\n\n# Additional Project Context\n\n{{gatheredContext}}\n{{/if}}"
|
||||
"user": "{{#if hasCodebaseAnalysis}}## IMPORTANT: Codebase Analysis Required\n\nYou have access to powerful codebase analysis tools. Before generating the subtask update:\n\n1. Use the Glob tool to explore the project structure (e.g., \"**/*.js\", \"**/*.json\", \"**/README.md\")\n2. Use the Grep tool to search for existing implementations, patterns, and technologies\n3. Use the Read tool to examine relevant files and understand current implementation\n4. Analyze the current codebase to inform your subtask update\n\nBased on your analysis:\n- Include specific file references, code patterns, or implementation details\n- Ensure suggestions align with the project's current architecture\n- Reference existing components or patterns when relevant\n- Make implementation notes specific to the codebase structure\n\nProject Root: {{projectRoot}}\n\n{{/if}}Task Context:\n\nParent Task: {{{json parentTask}}}\n{{#if prevSubtask}}Previous Subtask: {{{json prevSubtask}}}\n{{/if}}{{#if nextSubtask}}Next Subtask: {{{json nextSubtask}}}\n{{/if}}Current Subtask Details (for context only):\n{{currentDetails}}\n\nUser Request: \"{{updatePrompt}}\"\n\n{{#if useResearch}}Research and incorporate current best practices, latest stable versions, and proven approaches into your update. {{/if}}Based on the User Request and all the Task Context (including current subtask details provided above), what is the new information or text that should be appended to this subtask's details? Return ONLY this new text as a plain string.{{#if useResearch}} Include specific technical recommendations based on current industry standards.{{/if}}\n{{#if gatheredContext}}\n\n# Additional Project Context\n\n{{gatheredContext}}\n{{/if}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,17 +43,29 @@
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Additional project context"
|
||||
},
|
||||
"hasCodebaseAnalysis": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": false,
|
||||
"description": "Whether codebase analysis is available"
|
||||
},
|
||||
"projectRoot": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "",
|
||||
"description": "Project root path for context"
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
"default": {
|
||||
"system": "You are an AI assistant helping to update a software development task based on new context.{{#if useResearch}} You have access to current best practices and latest technical information to provide research-backed updates.{{/if}}\nYou will be given a task and a prompt describing changes or new implementation details.\nYour job is to update the task to reflect these changes, while preserving its basic structure.\n\nGuidelines:\n1. VERY IMPORTANT: NEVER change the title of the task - keep it exactly as is\n2. Maintain the same ID, status, and dependencies unless specifically mentioned in the prompt{{#if useResearch}}\n3. Research and update the description, details, and test strategy with current best practices\n4. Include specific versions, libraries, and approaches that are current and well-tested{{/if}}{{#if (not useResearch)}}\n3. Update the description, details, and test strategy to reflect the new information\n4. Do not change anything unnecessarily - just adapt what needs to change based on the prompt{{/if}}\n5. Return a complete valid JSON object representing the updated task\n6. VERY IMPORTANT: Preserve all subtasks marked as \"done\" or \"completed\" - do not modify their content\n7. For tasks with completed subtasks, build upon what has already been done rather than rewriting everything\n8. If an existing completed subtask needs to be changed/undone based on the new context, DO NOT modify it directly\n9. Instead, add a new subtask that clearly indicates what needs to be changed or replaced\n10. Use the existence of completed subtasks as an opportunity to make new subtasks more specific and targeted\n11. Ensure any new subtasks have unique IDs that don't conflict with existing ones\n12. CRITICAL: For subtask IDs, use ONLY numeric values (1, 2, 3, etc.) NOT strings (\"1\", \"2\", \"3\")\n13. CRITICAL: Subtask IDs should start from 1 and increment sequentially (1, 2, 3...) - do NOT use parent task ID as prefix{{#if useResearch}}\n14. Include links to documentation or resources where helpful\n15. Focus on practical, implementable solutions using current technologies{{/if}}\n\nThe changes described in the prompt should be thoughtfully applied to make the task more accurate and actionable.",
|
||||
"user": "Here is the task to update{{#if useResearch}} with research-backed information{{/if}}:\n{{{taskJson}}}\n\nPlease {{#if useResearch}}research and {{/if}}update this task based on the following {{#if useResearch}}context:\n{{updatePrompt}}\n\nIncorporate current best practices, latest stable versions, and proven approaches.{{/if}}{{#if (not useResearch)}}new context:\n{{updatePrompt}}{{/if}}\n\nIMPORTANT: {{#if useResearch}}Preserve any subtasks marked as \"done\" or \"completed\".{{/if}}{{#if (not useResearch)}}In the task JSON above, any subtasks with \"status\": \"done\" or \"status\": \"completed\" should be preserved exactly as is. Build your changes around these completed items.{{/if}}\n{{#if gatheredContext}}\n\n# Project Context\n\n{{gatheredContext}}\n{{/if}}\n\nReturn only the updated task as a valid JSON object{{#if useResearch}} with research-backed improvements{{/if}}."
|
||||
"user": "{{#if hasCodebaseAnalysis}}## IMPORTANT: Codebase Analysis Required\n\nYou have access to powerful codebase analysis tools. Before updating the task:\n\n1. Use the Glob tool to explore the project structure (e.g., \"**/*.js\", \"**/*.json\", \"**/README.md\")\n2. Use the Grep tool to search for existing implementations, patterns, and technologies\n3. Use the Read tool to examine relevant files and understand current implementation\n4. Analyze how the task changes relate to the existing codebase\n\nBased on your analysis:\n- Update task details to reference specific files, functions, or patterns from the codebase\n- Ensure implementation details align with the project's current architecture\n- Include specific code examples or file references where appropriate\n- Consider how changes impact existing components\n\nProject Root: {{projectRoot}}\n\n{{/if}}Here is the task to update{{#if useResearch}} with research-backed information{{/if}}:\n{{{taskJson}}}\n\nPlease {{#if useResearch}}research and {{/if}}update this task based on the following {{#if useResearch}}context:\n{{updatePrompt}}\n\nIncorporate current best practices, latest stable versions, and proven approaches.{{/if}}{{#if (not useResearch)}}new context:\n{{updatePrompt}}{{/if}}\n\nIMPORTANT: {{#if useResearch}}Preserve any subtasks marked as \"done\" or \"completed\".{{/if}}{{#if (not useResearch)}}In the task JSON above, any subtasks with \"status\": \"done\" or \"status\": \"completed\" should be preserved exactly as is. Build your changes around these completed items.{{/if}}\n{{#if gatheredContext}}\n\n# Project Context\n\n{{gatheredContext}}\n{{/if}}\n\nReturn only the updated task as a valid JSON object{{#if useResearch}} with research-backed improvements{{/if}}."
|
||||
},
|
||||
"append": {
|
||||
"condition": "appendMode === true",
|
||||
"system": "You are an AI assistant helping to append additional information to a software development task. You will be provided with the task's existing details, context, and a user request string.\n\nYour Goal: Based *only* on the user's request and all the provided context (including existing details if relevant to the request), GENERATE the new text content that should be added to the task's details.\nFocus *only* on generating the substance of the update.\n\nOutput Requirements:\n1. Return *only* the newly generated text content as a plain string. Do NOT return a JSON object or any other structured data.\n2. Your string response should NOT include any of the task's original details, unless the user's request explicitly asks to rephrase, summarize, or directly modify existing text.\n3. Do NOT include any timestamps, XML-like tags, markdown, or any other special formatting in your string response.\n4. Ensure the generated text is concise yet complete for the update based on the user request. Avoid conversational fillers or explanations about what you are doing (e.g., do not start with \"Okay, here's the update...\").",
|
||||
"user": "Task Context:\n\nTask: {{{json task}}}\nCurrent Task Details (for context only):\n{{currentDetails}}\n\nUser Request: \"{{updatePrompt}}\"\n\nBased on the User Request and all the Task Context (including current task details provided above), what is the new information or text that should be appended to this task's details? Return ONLY this new text as a plain string.\n{{#if gatheredContext}}\n\n# Additional Project Context\n\n{{gatheredContext}}\n{{/if}}"
|
||||
"user": "{{#if hasCodebaseAnalysis}}## IMPORTANT: Codebase Analysis Required\n\nYou have access to powerful codebase analysis tools. Before generating the task update:\n\n1. Use the Glob tool to explore the project structure (e.g., \"**/*.js\", \"**/*.json\", \"**/README.md\")\n2. Use the Grep tool to search for existing implementations, patterns, and technologies\n3. Use the Read tool to examine relevant files and understand current implementation\n4. Analyze the current codebase to inform your update\n\nBased on your analysis:\n- Include specific file references, code patterns, or implementation details\n- Ensure suggestions align with the project's current architecture\n- Reference existing components or patterns when relevant\n\nProject Root: {{projectRoot}}\n\n{{/if}}Task Context:\n\nTask: {{{json task}}}\nCurrent Task Details (for context only):\n{{currentDetails}}\n\nUser Request: \"{{updatePrompt}}\"\n\nBased on the User Request and all the Task Context (including current task details provided above), what is the new information or text that should be appended to this task's details? Return ONLY this new text as a plain string.\n{{#if gatheredContext}}\n\n# Additional Project Context\n\n{{gatheredContext}}\n{{/if}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,12 +27,24 @@
|
||||
"projectContext": {
|
||||
"type": "string",
|
||||
"description": "Additional project context"
|
||||
},
|
||||
"hasCodebaseAnalysis": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": false,
|
||||
"description": "Whether codebase analysis is available"
|
||||
},
|
||||
"projectRoot": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "",
|
||||
"description": "Project root path for context"
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
"default": {
|
||||
"system": "You are an AI assistant helping to update software development tasks based on new context.\nYou will be given a set of tasks and a prompt describing changes or new implementation details.\nYour job is to update the tasks to reflect these changes, while preserving their basic structure.\n\nCRITICAL RULES:\n1. Return ONLY a JSON array - no explanations, no markdown, no additional text before or after\n2. Each task MUST have ALL fields from the original (do not omit any fields)\n3. Maintain the same IDs, statuses, and dependencies unless specifically mentioned in the prompt\n4. Update titles, descriptions, details, and test strategies to reflect the new information\n5. Do not change anything unnecessarily - just adapt what needs to change based on the prompt\n6. You should return ALL the tasks in order, not just the modified ones\n7. Return a complete valid JSON array with all tasks\n8. VERY IMPORTANT: Preserve all subtasks marked as \"done\" or \"completed\" - do not modify their content\n9. For tasks with completed subtasks, build upon what has already been done rather than rewriting everything\n10. If an existing completed subtask needs to be changed/undone based on the new context, DO NOT modify it directly\n11. Instead, add a new subtask that clearly indicates what needs to be changed or replaced\n12. Use the existence of completed subtasks as an opportunity to make new subtasks more specific and targeted\n\nThe changes described in the prompt should be applied to ALL tasks in the list.",
|
||||
"user": "Here are the tasks to update:\n{{{json tasks}}}\n\nPlease update these tasks based on the following new context:\n{{updatePrompt}}\n\nIMPORTANT: In the tasks JSON above, any subtasks with \"status\": \"done\" or \"status\": \"completed\" should be preserved exactly as is. Build your changes around these completed items.{{#if projectContext}}\n\n# Project Context\n\n{{projectContext}}{{/if}}\n\nRequired JSON structure for EACH task (ALL fields MUST be present):\n{\n \"id\": <number>,\n \"title\": <string>,\n \"description\": <string>,\n \"status\": <string>,\n \"dependencies\": <array>,\n \"priority\": <string or null>,\n \"details\": <string or null>,\n \"testStrategy\": <string or null>,\n \"subtasks\": <array or null>\n}\n\nReturn a valid JSON array containing ALL the tasks with ALL their fields:\n- id (number) - preserve existing value\n- title (string)\n- description (string)\n- status (string) - preserve existing value unless explicitly changing\n- dependencies (array) - preserve existing value unless explicitly changing\n- priority (string or null)\n- details (string or null)\n- testStrategy (string or null)\n- subtasks (array or null)\n\nReturn ONLY the JSON array now:"
|
||||
"user": "{{#if hasCodebaseAnalysis}}## IMPORTANT: Codebase Analysis Required\n\nYou have access to powerful codebase analysis tools. Before updating tasks:\n\n1. Use the Glob tool to explore the project structure (e.g., \"**/*.js\", \"**/*.json\", \"**/README.md\")\n2. Use the Grep tool to search for existing implementations, patterns, and technologies\n3. Use the Read tool to examine relevant files and understand current implementation\n4. Analyze how the new changes relate to the existing codebase\n\nBased on your analysis:\n- Update task details to reference specific files, functions, or patterns from the codebase\n- Ensure implementation details align with the project's current architecture\n- Include specific code examples or file references where appropriate\n- Consider how changes impact existing components\n\nProject Root: {{projectRoot}}\n\n{{/if}}Here are the tasks to update:\n{{{json tasks}}}\n\nPlease update these tasks based on the following new context:\n{{updatePrompt}}\n\nIMPORTANT: In the tasks JSON above, any subtasks with \"status\": \"done\" or \"status\": \"completed\" should be preserved exactly as is. Build your changes around these completed items.{{#if projectContext}}\n\n# Project Context\n\n{{projectContext}}{{/if}}\n\nRequired JSON structure for EACH task (ALL fields MUST be present):\n{\n \"id\": <number>,\n \"title\": <string>,\n \"description\": <string>,\n \"status\": <string>,\n \"dependencies\": <array>,\n \"priority\": <string or null>,\n \"details\": <string or null>,\n \"testStrategy\": <string or null>,\n \"subtasks\": <array or null>\n}\n\nReturn a valid JSON array containing ALL the tasks with ALL their fields:\n- id (number) - preserve existing value\n- title (string)\n- description (string)\n- status (string) - preserve existing value unless explicitly changing\n- dependencies (array) - preserve existing value unless explicitly changing\n- priority (string or null)\n- details (string or null)\n- testStrategy (string or null)\n- subtasks (array or null)\n\nReturn ONLY the JSON array now:"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
273
src/ui/indicators.js
Normal file
273
src/ui/indicators.js
Normal file
@@ -0,0 +1,273 @@
|
||||
/**
|
||||
* indicators.js
|
||||
* UI functions for displaying priority and complexity indicators in different contexts
|
||||
*/
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { TASK_PRIORITY_OPTIONS } from '../constants/task-priority.js';
|
||||
|
||||
// Extract priority values for cleaner object keys
|
||||
const [HIGH, MEDIUM, LOW] = TASK_PRIORITY_OPTIONS;
|
||||
|
||||
// Cache for generated indicators
|
||||
const INDICATOR_CACHE = new Map();
|
||||
|
||||
/**
|
||||
* Base configuration for indicator systems
|
||||
*/
|
||||
class IndicatorConfig {
|
||||
constructor(name, levels, colors, thresholds = null) {
|
||||
this.name = name;
|
||||
this.levels = levels;
|
||||
this.colors = colors;
|
||||
this.thresholds = thresholds;
|
||||
}
|
||||
|
||||
getColor(level) {
|
||||
return this.colors[level] || chalk.gray;
|
||||
}
|
||||
|
||||
getLevelFromScore(score) {
|
||||
if (!this.thresholds) {
|
||||
throw new Error(`${this.name} does not support score-based levels`);
|
||||
}
|
||||
|
||||
if (score >= 7) return this.levels[0]; // high
|
||||
if (score <= 3) return this.levels[2]; // low
|
||||
return this.levels[1]; // medium
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Visual style definitions
|
||||
*/
|
||||
const VISUAL_STYLES = {
|
||||
cli: {
|
||||
filled: '●', // ●
|
||||
empty: '○' // ○
|
||||
},
|
||||
statusBar: {
|
||||
high: '⋮', // ⋮
|
||||
medium: ':', // :
|
||||
low: '.' // .
|
||||
},
|
||||
mcp: {
|
||||
high: '🔴', // 🔴
|
||||
medium: '🟠', // 🟠
|
||||
low: '🟢' // 🟢
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Priority configuration
|
||||
*/
|
||||
const PRIORITY_CONFIG = new IndicatorConfig('priority', [HIGH, MEDIUM, LOW], {
|
||||
[HIGH]: chalk.hex('#CC0000'),
|
||||
[MEDIUM]: chalk.hex('#FF8800'),
|
||||
[LOW]: chalk.yellow
|
||||
});
|
||||
|
||||
/**
|
||||
* Generates CLI indicator with intensity
|
||||
*/
|
||||
function generateCliIndicator(intensity, color) {
|
||||
const filled = VISUAL_STYLES.cli.filled;
|
||||
const empty = VISUAL_STYLES.cli.empty;
|
||||
|
||||
let indicator = '';
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (i < intensity) {
|
||||
indicator += color(filled);
|
||||
} else {
|
||||
indicator += chalk.white(empty);
|
||||
}
|
||||
}
|
||||
return indicator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get intensity level from priority/complexity level
|
||||
*/
|
||||
function getIntensityFromLevel(level, levels) {
|
||||
const index = levels.indexOf(level);
|
||||
return 3 - index; // high=3, medium=2, low=1
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic cached indicator getter
|
||||
* @param {string} cacheKey - Cache key for the indicators
|
||||
* @param {Function} generator - Function to generate the indicators
|
||||
* @returns {Object} Cached or newly generated indicators
|
||||
*/
|
||||
function getCachedIndicators(cacheKey, generator) {
|
||||
if (INDICATOR_CACHE.has(cacheKey)) {
|
||||
return INDICATOR_CACHE.get(cacheKey);
|
||||
}
|
||||
|
||||
const indicators = generator();
|
||||
INDICATOR_CACHE.set(cacheKey, indicators);
|
||||
return indicators;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get priority indicators for MCP context (single emojis)
|
||||
* @returns {Object} Priority to emoji mapping
|
||||
*/
|
||||
export function getMcpPriorityIndicators() {
|
||||
return getCachedIndicators('mcp-priority-all', () => ({
|
||||
[HIGH]: VISUAL_STYLES.mcp.high,
|
||||
[MEDIUM]: VISUAL_STYLES.mcp.medium,
|
||||
[LOW]: VISUAL_STYLES.mcp.low
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get priority indicators for CLI context (colored dots with visual hierarchy)
|
||||
* @returns {Object} Priority to colored dot string mapping
|
||||
*/
|
||||
export function getCliPriorityIndicators() {
|
||||
return getCachedIndicators('cli-priority-all', () => {
|
||||
const indicators = {};
|
||||
PRIORITY_CONFIG.levels.forEach((level) => {
|
||||
const intensity = getIntensityFromLevel(level, PRIORITY_CONFIG.levels);
|
||||
const color = PRIORITY_CONFIG.getColor(level);
|
||||
indicators[level] = generateCliIndicator(intensity, color);
|
||||
});
|
||||
return indicators;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get priority indicators for status bars (simplified single character versions)
|
||||
* @returns {Object} Priority to single character indicator mapping
|
||||
*/
|
||||
export function getStatusBarPriorityIndicators() {
|
||||
return getCachedIndicators('statusbar-priority-all', () => {
|
||||
const indicators = {};
|
||||
PRIORITY_CONFIG.levels.forEach((level, index) => {
|
||||
const style =
|
||||
index === 0
|
||||
? VISUAL_STYLES.statusBar.high
|
||||
: index === 1
|
||||
? VISUAL_STYLES.statusBar.medium
|
||||
: VISUAL_STYLES.statusBar.low;
|
||||
const color = PRIORITY_CONFIG.getColor(level);
|
||||
indicators[level] = color(style);
|
||||
});
|
||||
return indicators;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get priority colors for consistent styling
|
||||
* @returns {Object} Priority to chalk color function mapping
|
||||
*/
|
||||
export function getPriorityColors() {
|
||||
return {
|
||||
[HIGH]: PRIORITY_CONFIG.colors[HIGH],
|
||||
[MEDIUM]: PRIORITY_CONFIG.colors[MEDIUM],
|
||||
[LOW]: PRIORITY_CONFIG.colors[LOW]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get priority indicators based on context
|
||||
* @param {boolean} isMcp - Whether this is for MCP context (true) or CLI context (false)
|
||||
* @returns {Object} Priority to indicator mapping
|
||||
*/
|
||||
export function getPriorityIndicators(isMcp = false) {
|
||||
return isMcp ? getMcpPriorityIndicators() : getCliPriorityIndicators();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific priority indicator
|
||||
* @param {string} priority - The priority level ('high', 'medium', 'low')
|
||||
* @param {boolean} isMcp - Whether this is for MCP context
|
||||
* @returns {string} The indicator string for the priority
|
||||
*/
|
||||
export function getPriorityIndicator(priority, isMcp = false) {
|
||||
const indicators = getPriorityIndicators(isMcp);
|
||||
return indicators[priority] || indicators[MEDIUM];
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Complexity Indicators
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Complexity configuration
|
||||
*/
|
||||
const COMPLEXITY_CONFIG = new IndicatorConfig(
|
||||
'complexity',
|
||||
['high', 'medium', 'low'],
|
||||
{
|
||||
high: chalk.hex('#CC0000'),
|
||||
medium: chalk.hex('#FF8800'),
|
||||
low: chalk.green
|
||||
},
|
||||
{
|
||||
high: (score) => score >= 7,
|
||||
medium: (score) => score >= 4 && score <= 6,
|
||||
low: (score) => score <= 3
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Get complexity indicators for CLI context (colored dots with visual hierarchy)
|
||||
* Complexity scores: 1-3 (low), 4-6 (medium), 7-10 (high)
|
||||
* @returns {Object} Complexity level to colored dot string mapping
|
||||
*/
|
||||
export function getCliComplexityIndicators() {
|
||||
return getCachedIndicators('cli-complexity-all', () => {
|
||||
const indicators = {};
|
||||
COMPLEXITY_CONFIG.levels.forEach((level) => {
|
||||
const intensity = getIntensityFromLevel(level, COMPLEXITY_CONFIG.levels);
|
||||
const color = COMPLEXITY_CONFIG.getColor(level);
|
||||
indicators[level] = generateCliIndicator(intensity, color);
|
||||
});
|
||||
return indicators;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get complexity indicators for status bars (simplified single character versions)
|
||||
* @returns {Object} Complexity level to single character indicator mapping
|
||||
*/
|
||||
export function getStatusBarComplexityIndicators() {
|
||||
return getCachedIndicators('statusbar-complexity-all', () => {
|
||||
const indicators = {};
|
||||
COMPLEXITY_CONFIG.levels.forEach((level, index) => {
|
||||
const style =
|
||||
index === 0
|
||||
? VISUAL_STYLES.statusBar.high
|
||||
: index === 1
|
||||
? VISUAL_STYLES.statusBar.medium
|
||||
: VISUAL_STYLES.statusBar.low;
|
||||
const color = COMPLEXITY_CONFIG.getColor(level);
|
||||
indicators[level] = color(style);
|
||||
});
|
||||
return indicators;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get complexity colors for consistent styling
|
||||
* @returns {Object} Complexity level to chalk color function mapping
|
||||
*/
|
||||
export function getComplexityColors() {
|
||||
return { ...COMPLEXITY_CONFIG.colors };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific complexity indicator based on score
|
||||
* @param {number} score - The complexity score (1-10)
|
||||
* @param {boolean} statusBar - Whether to return status bar version (single char)
|
||||
* @returns {string} The indicator string for the complexity level
|
||||
*/
|
||||
export function getComplexityIndicator(score, statusBar = false) {
|
||||
const level = COMPLEXITY_CONFIG.getLevelFromScore(score);
|
||||
const indicators = statusBar
|
||||
? getStatusBarComplexityIndicators()
|
||||
: getCliComplexityIndicators();
|
||||
return indicators[level];
|
||||
}
|
||||
477
src/ui/parse-prd.js
Normal file
477
src/ui/parse-prd.js
Normal file
@@ -0,0 +1,477 @@
|
||||
/**
|
||||
* parse-prd.js
|
||||
* UI functions specifically for PRD parsing operations
|
||||
*/
|
||||
|
||||
import chalk from 'chalk';
|
||||
import boxen from 'boxen';
|
||||
import Table from 'cli-table3';
|
||||
import { formatElapsedTime } from '../utils/format.js';
|
||||
|
||||
// Constants
|
||||
const CONSTANTS = {
|
||||
BAR_WIDTH: 40,
|
||||
TABLE_COL_WIDTHS: [28, 50],
|
||||
DEFAULT_MODEL: 'Default',
|
||||
DEFAULT_TEMPERATURE: 0.7
|
||||
};
|
||||
|
||||
const PRIORITIES = {
|
||||
HIGH: 'high',
|
||||
MEDIUM: 'medium',
|
||||
LOW: 'low'
|
||||
};
|
||||
|
||||
const PRIORITY_COLORS = {
|
||||
[PRIORITIES.HIGH]: '#CC0000',
|
||||
[PRIORITIES.MEDIUM]: '#FF8800',
|
||||
[PRIORITIES.LOW]: '#FFCC00'
|
||||
};
|
||||
|
||||
// Reusable box styles
|
||||
const BOX_STYLES = {
|
||||
main: {
|
||||
padding: { top: 1, bottom: 1, left: 2, right: 2 },
|
||||
margin: { top: 0, bottom: 0 },
|
||||
borderColor: 'blue',
|
||||
borderStyle: 'round'
|
||||
},
|
||||
summary: {
|
||||
padding: { top: 1, right: 1, bottom: 1, left: 1 },
|
||||
borderColor: 'blue',
|
||||
borderStyle: 'round',
|
||||
margin: { top: 1, right: 1, bottom: 1, left: 0 }
|
||||
},
|
||||
warning: {
|
||||
padding: 1,
|
||||
borderColor: 'yellow',
|
||||
borderStyle: 'round',
|
||||
margin: { top: 1, bottom: 1 }
|
||||
},
|
||||
nextSteps: {
|
||||
padding: 1,
|
||||
borderColor: 'cyan',
|
||||
borderStyle: 'round',
|
||||
margin: { top: 1, right: 0, bottom: 1, left: 0 }
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function for building main message content
|
||||
* @param {Object} params - Message parameters
|
||||
* @param {string} params.prdFilePath - Path to the PRD file
|
||||
* @param {string} params.outputPath - Path where tasks will be saved
|
||||
* @param {number} params.numTasks - Number of tasks to generate
|
||||
* @param {string} params.model - AI model name
|
||||
* @param {number} params.temperature - AI temperature setting
|
||||
* @param {boolean} params.append - Whether appending to existing tasks
|
||||
* @param {boolean} params.research - Whether research mode is enabled
|
||||
* @returns {string} The formatted message content
|
||||
*/
|
||||
function buildMainMessage({
|
||||
prdFilePath,
|
||||
outputPath,
|
||||
numTasks,
|
||||
model,
|
||||
temperature,
|
||||
append,
|
||||
research
|
||||
}) {
|
||||
const actionVerb = append ? 'Appending' : 'Generating';
|
||||
|
||||
let modelLine = `Model: ${model} | Temperature: ${temperature}`;
|
||||
if (research) {
|
||||
modelLine += ` | ${chalk.cyan.bold('🔬 Research Mode')}`;
|
||||
}
|
||||
|
||||
return (
|
||||
chalk.bold(`🤖 Parsing PRD and ${actionVerb} Tasks`) +
|
||||
'\n' +
|
||||
chalk.dim(modelLine) +
|
||||
'\n\n' +
|
||||
chalk.blue(`Input: ${prdFilePath}`) +
|
||||
'\n' +
|
||||
chalk.blue(`Output: ${outputPath}`) +
|
||||
'\n' +
|
||||
chalk.blue(`Tasks to ${append ? 'Append' : 'Generate'}: ${numTasks}`)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for displaying the main message box
|
||||
* @param {string} message - The message content to display in the box
|
||||
*/
|
||||
function displayMainMessageBox(message) {
|
||||
console.log(boxen(message, BOX_STYLES.main));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for displaying append mode notice
|
||||
* @param {number} existingTasksCount - Number of existing tasks
|
||||
* @param {number} nextId - Next ID to be used
|
||||
*/
|
||||
function displayAppendModeNotice(existingTasksCount, nextId) {
|
||||
console.log(
|
||||
chalk.yellow.bold('📝 Append mode') +
|
||||
` - Adding to ${existingTasksCount} existing tasks (next ID: ${nextId})`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for force mode messages
|
||||
* @param {boolean} append - Whether in append mode
|
||||
* @returns {string} The formatted force mode message
|
||||
*/
|
||||
function createForceMessage(append) {
|
||||
const baseMessage = chalk.red.bold('⚠️ Force flag enabled');
|
||||
return append
|
||||
? `${baseMessage} - Will overwrite if conflicts occur`
|
||||
: `${baseMessage} - Overwriting existing tasks`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the start of PRD parsing with a boxen announcement
|
||||
* @param {Object} options - Options for PRD parsing start
|
||||
* @param {string} options.prdFilePath - Path to the PRD file being parsed
|
||||
* @param {string} options.outputPath - Path where the tasks will be saved
|
||||
* @param {number} options.numTasks - Number of tasks to generate
|
||||
* @param {string} [options.model] - AI model name
|
||||
* @param {number} [options.temperature] - AI temperature setting
|
||||
* @param {boolean} [options.append=false] - Whether to append to existing tasks
|
||||
* @param {boolean} [options.research=false] - Whether research mode is enabled
|
||||
* @param {boolean} [options.force=false] - Whether force mode is enabled
|
||||
* @param {Array} [options.existingTasks=[]] - Existing tasks array
|
||||
* @param {number} [options.nextId=1] - Next ID to be used
|
||||
*/
|
||||
function displayParsePrdStart({
|
||||
prdFilePath,
|
||||
outputPath,
|
||||
numTasks,
|
||||
model = CONSTANTS.DEFAULT_MODEL,
|
||||
temperature = CONSTANTS.DEFAULT_TEMPERATURE,
|
||||
append = false,
|
||||
research = false,
|
||||
force = false,
|
||||
existingTasks = [],
|
||||
nextId = 1
|
||||
}) {
|
||||
// Input validation
|
||||
if (
|
||||
!prdFilePath ||
|
||||
typeof prdFilePath !== 'string' ||
|
||||
prdFilePath.trim() === ''
|
||||
) {
|
||||
throw new Error('prdFilePath is required and must be a non-empty string');
|
||||
}
|
||||
if (
|
||||
!outputPath ||
|
||||
typeof outputPath !== 'string' ||
|
||||
outputPath.trim() === ''
|
||||
) {
|
||||
throw new Error('outputPath is required and must be a non-empty string');
|
||||
}
|
||||
|
||||
// Build and display the main message box
|
||||
const message = buildMainMessage({
|
||||
prdFilePath,
|
||||
outputPath,
|
||||
numTasks,
|
||||
model,
|
||||
temperature,
|
||||
append,
|
||||
research
|
||||
});
|
||||
displayMainMessageBox(message);
|
||||
|
||||
// Display append/force notices beneath the boxen if either flag is set
|
||||
if (append || force) {
|
||||
// Add append mode details if enabled
|
||||
if (append) {
|
||||
displayAppendModeNotice(existingTasks.length, nextId);
|
||||
}
|
||||
|
||||
// Add force mode details if enabled
|
||||
if (force) {
|
||||
console.log(createForceMessage(append));
|
||||
}
|
||||
|
||||
// Add a blank line after notices for spacing
|
||||
console.log();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate priority statistics
|
||||
* @param {Object} taskPriorities - Priority counts object
|
||||
* @param {number} totalTasks - Total number of tasks
|
||||
* @returns {Object} Priority statistics with counts and percentages
|
||||
*/
|
||||
function calculatePriorityStats(taskPriorities, totalTasks) {
|
||||
const stats = {};
|
||||
|
||||
Object.values(PRIORITIES).forEach((priority) => {
|
||||
const count = taskPriorities[priority] || 0;
|
||||
stats[priority] = {
|
||||
count,
|
||||
percentage: totalTasks > 0 ? Math.round((count / totalTasks) * 100) : 0
|
||||
};
|
||||
});
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate bar character distribution for priorities
|
||||
* @param {Object} priorityStats - Priority statistics
|
||||
* @param {number} totalTasks - Total number of tasks
|
||||
* @returns {Object} Character counts for each priority
|
||||
*/
|
||||
function calculateBarDistribution(priorityStats, totalTasks) {
|
||||
const barWidth = CONSTANTS.BAR_WIDTH;
|
||||
const distribution = {};
|
||||
|
||||
if (totalTasks === 0) {
|
||||
Object.values(PRIORITIES).forEach((priority) => {
|
||||
distribution[priority] = 0;
|
||||
});
|
||||
return distribution;
|
||||
}
|
||||
|
||||
// Calculate raw proportions
|
||||
const rawChars = {};
|
||||
Object.values(PRIORITIES).forEach((priority) => {
|
||||
rawChars[priority] =
|
||||
(priorityStats[priority].count / totalTasks) * barWidth;
|
||||
});
|
||||
|
||||
// Initial distribution - floor values
|
||||
Object.values(PRIORITIES).forEach((priority) => {
|
||||
distribution[priority] = Math.floor(rawChars[priority]);
|
||||
});
|
||||
|
||||
// Ensure non-zero priorities get at least 1 character
|
||||
Object.values(PRIORITIES).forEach((priority) => {
|
||||
if (priorityStats[priority].count > 0 && distribution[priority] === 0) {
|
||||
distribution[priority] = 1;
|
||||
}
|
||||
});
|
||||
|
||||
// Distribute remaining characters based on decimal parts
|
||||
const currentTotal = Object.values(distribution).reduce(
|
||||
(sum, val) => sum + val,
|
||||
0
|
||||
);
|
||||
const remainingChars = barWidth - currentTotal;
|
||||
|
||||
if (remainingChars > 0) {
|
||||
const decimals = Object.values(PRIORITIES)
|
||||
.map((priority) => ({
|
||||
priority,
|
||||
decimal: rawChars[priority] - Math.floor(rawChars[priority])
|
||||
}))
|
||||
.sort((a, b) => b.decimal - a.decimal);
|
||||
|
||||
for (let i = 0; i < remainingChars && i < decimals.length; i++) {
|
||||
distribution[decimals[i].priority]++;
|
||||
}
|
||||
}
|
||||
|
||||
return distribution;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create priority distribution bar visual
|
||||
* @param {Object} barDistribution - Character distribution for priorities
|
||||
* @returns {string} Visual bar string
|
||||
*/
|
||||
function createPriorityBar(barDistribution) {
|
||||
let bar = '';
|
||||
|
||||
bar += chalk.hex(PRIORITY_COLORS[PRIORITIES.HIGH])(
|
||||
'█'.repeat(barDistribution[PRIORITIES.HIGH])
|
||||
);
|
||||
bar += chalk.hex(PRIORITY_COLORS[PRIORITIES.MEDIUM])(
|
||||
'█'.repeat(barDistribution[PRIORITIES.MEDIUM])
|
||||
);
|
||||
bar += chalk.yellow('█'.repeat(barDistribution[PRIORITIES.LOW]));
|
||||
|
||||
const totalChars = Object.values(barDistribution).reduce(
|
||||
(sum, val) => sum + val,
|
||||
0
|
||||
);
|
||||
if (totalChars < CONSTANTS.BAR_WIDTH) {
|
||||
bar += chalk.gray('░'.repeat(CONSTANTS.BAR_WIDTH - totalChars));
|
||||
}
|
||||
|
||||
return bar;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build priority distribution row for table
|
||||
* @param {Object} priorityStats - Priority statistics
|
||||
* @returns {Array} Table row for priority distribution
|
||||
*/
|
||||
function buildPriorityRow(priorityStats) {
|
||||
const parts = [];
|
||||
|
||||
Object.entries(PRIORITIES).forEach(([key, priority]) => {
|
||||
const stats = priorityStats[priority];
|
||||
const color =
|
||||
priority === PRIORITIES.HIGH
|
||||
? chalk.hex(PRIORITY_COLORS[PRIORITIES.HIGH])
|
||||
: priority === PRIORITIES.MEDIUM
|
||||
? chalk.hex(PRIORITY_COLORS[PRIORITIES.MEDIUM])
|
||||
: chalk.yellow;
|
||||
|
||||
const label = key.charAt(0) + key.slice(1).toLowerCase();
|
||||
parts.push(
|
||||
`${color.bold(stats.count)} ${color(label)} (${stats.percentage}%)`
|
||||
);
|
||||
});
|
||||
|
||||
return [chalk.cyan('Priority distribution:'), parts.join(' · ')];
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a summary of the PRD parsing results
|
||||
* @param {Object} summary - Summary of the parsing results
|
||||
* @param {number} summary.totalTasks - Total number of tasks generated
|
||||
* @param {string} summary.prdFilePath - Path to the PRD file
|
||||
* @param {string} summary.outputPath - Path where the tasks were saved
|
||||
* @param {number} summary.elapsedTime - Total elapsed time in seconds
|
||||
* @param {Object} summary.taskPriorities - Breakdown of tasks by category/priority
|
||||
* @param {boolean} summary.usedFallback - Whether fallback parsing was used
|
||||
* @param {string} summary.actionVerb - Whether tasks were 'generated' or 'appended'
|
||||
*/
|
||||
function displayParsePrdSummary(summary) {
|
||||
const {
|
||||
totalTasks,
|
||||
taskPriorities = {},
|
||||
prdFilePath,
|
||||
outputPath,
|
||||
elapsedTime,
|
||||
usedFallback = false,
|
||||
actionVerb = 'generated'
|
||||
} = summary;
|
||||
|
||||
// Format the elapsed time
|
||||
const timeDisplay = formatElapsedTime(elapsedTime);
|
||||
|
||||
// Create a table for better alignment
|
||||
const table = new Table({
|
||||
chars: {
|
||||
top: '',
|
||||
'top-mid': '',
|
||||
'top-left': '',
|
||||
'top-right': '',
|
||||
bottom: '',
|
||||
'bottom-mid': '',
|
||||
'bottom-left': '',
|
||||
'bottom-right': '',
|
||||
left: '',
|
||||
'left-mid': '',
|
||||
mid: '',
|
||||
'mid-mid': '',
|
||||
right: '',
|
||||
'right-mid': '',
|
||||
middle: ' '
|
||||
},
|
||||
style: { border: [], 'padding-left': 2 },
|
||||
colWidths: CONSTANTS.TABLE_COL_WIDTHS
|
||||
});
|
||||
|
||||
// Basic info
|
||||
// Use the action verb to properly display if tasks were generated or appended
|
||||
table.push(
|
||||
[chalk.cyan(`Total tasks ${actionVerb}:`), chalk.bold(totalTasks)],
|
||||
[chalk.cyan('Processing time:'), chalk.bold(timeDisplay)]
|
||||
);
|
||||
|
||||
// Priority distribution if available
|
||||
if (taskPriorities && Object.keys(taskPriorities).length > 0) {
|
||||
const priorityStats = calculatePriorityStats(taskPriorities, totalTasks);
|
||||
const priorityRow = buildPriorityRow(priorityStats);
|
||||
table.push(priorityRow);
|
||||
|
||||
// Visual bar representation
|
||||
const barDistribution = calculateBarDistribution(priorityStats, totalTasks);
|
||||
const distributionBar = createPriorityBar(barDistribution);
|
||||
table.push([chalk.cyan('Distribution:'), distributionBar]);
|
||||
}
|
||||
|
||||
// Add file paths
|
||||
table.push(
|
||||
[chalk.cyan('PRD source:'), chalk.italic(prdFilePath)],
|
||||
[chalk.cyan('Tasks file:'), chalk.italic(outputPath)]
|
||||
);
|
||||
|
||||
// Add fallback parsing indicator if applicable
|
||||
if (usedFallback) {
|
||||
table.push([
|
||||
chalk.yellow('Fallback parsing:'),
|
||||
chalk.yellow('✓ Used fallback parsing')
|
||||
]);
|
||||
}
|
||||
|
||||
// Final string output with title and footer
|
||||
const output = [
|
||||
chalk.bold.underline(
|
||||
`PRD Parsing Complete - Tasks ${actionVerb.charAt(0).toUpperCase() + actionVerb.slice(1)}`
|
||||
),
|
||||
'',
|
||||
table.toString()
|
||||
].join('\n');
|
||||
|
||||
// Display the summary box
|
||||
console.log(boxen(output, BOX_STYLES.summary));
|
||||
|
||||
// Show fallback parsing warning if needed
|
||||
if (usedFallback) {
|
||||
displayFallbackWarning();
|
||||
}
|
||||
|
||||
// Show next steps
|
||||
displayNextSteps();
|
||||
}
|
||||
|
||||
/**
|
||||
* Display fallback parsing warning
|
||||
*/
|
||||
function displayFallbackWarning() {
|
||||
const warningContent =
|
||||
chalk.yellow.bold('⚠️ Fallback Parsing Used') +
|
||||
'\n\n' +
|
||||
chalk.white(
|
||||
'The system used fallback parsing to complete task generation.'
|
||||
) +
|
||||
'\n' +
|
||||
chalk.white(
|
||||
'This typically happens when streaming JSON parsing is incomplete.'
|
||||
) +
|
||||
'\n' +
|
||||
chalk.white('Your tasks were successfully generated, but consider:') +
|
||||
'\n' +
|
||||
chalk.white('• Reviewing task completeness') +
|
||||
'\n' +
|
||||
chalk.white('• Checking for any missing details') +
|
||||
'\n\n' +
|
||||
chalk.white("This is normal and usually doesn't indicate any issues.");
|
||||
|
||||
console.log(boxen(warningContent, BOX_STYLES.warning));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display next steps after parsing
|
||||
*/
|
||||
function displayNextSteps() {
|
||||
const stepsContent =
|
||||
chalk.white.bold('Next Steps:') +
|
||||
'\n\n' +
|
||||
`${chalk.cyan('1.')} Run ${chalk.yellow('task-master list')} to view all tasks\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 analyze-complexity')} to analyze task complexity`;
|
||||
|
||||
console.log(boxen(stepsContent, BOX_STYLES.nextSteps));
|
||||
}
|
||||
|
||||
export { displayParsePrdStart, displayParsePrdSummary, formatElapsedTime };
|
||||
12
src/utils/format.js
Normal file
12
src/utils/format.js
Normal file
@@ -0,0 +1,12 @@
|
||||
// src/utils/format.js
|
||||
|
||||
/**
|
||||
* Formats elapsed time as 0m 00s.
|
||||
* @param {number} seconds - Elapsed time in seconds
|
||||
* @returns {string} Formatted time string
|
||||
*/
|
||||
export function formatElapsedTime(seconds) {
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = Math.floor(seconds % 60);
|
||||
return `${minutes}m ${remainingSeconds.toString().padStart(2, '0')}s`;
|
||||
}
|
||||
490
src/utils/stream-parser.js
Normal file
490
src/utils/stream-parser.js
Normal file
@@ -0,0 +1,490 @@
|
||||
import { JSONParser } from '@streamparser/json';
|
||||
|
||||
/**
|
||||
* Custom error class for streaming-related failures
|
||||
* Provides error codes for robust error handling without string matching
|
||||
*/
|
||||
export class StreamingError extends Error {
|
||||
constructor(message, code) {
|
||||
super(message);
|
||||
this.name = 'StreamingError';
|
||||
this.code = code;
|
||||
|
||||
// Maintain proper stack trace (V8 engines)
|
||||
if (Error.captureStackTrace) {
|
||||
Error.captureStackTrace(this, StreamingError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard streaming error codes
|
||||
*/
|
||||
export const STREAMING_ERROR_CODES = {
|
||||
NOT_ASYNC_ITERABLE: 'STREAMING_NOT_SUPPORTED',
|
||||
STREAM_PROCESSING_FAILED: 'STREAM_PROCESSING_FAILED',
|
||||
STREAM_NOT_ITERABLE: 'STREAM_NOT_ITERABLE',
|
||||
BUFFER_SIZE_EXCEEDED: 'BUFFER_SIZE_EXCEEDED'
|
||||
};
|
||||
|
||||
/**
|
||||
* Default maximum buffer size (1MB)
|
||||
*/
|
||||
export const DEFAULT_MAX_BUFFER_SIZE = 1024 * 1024; // 1MB in bytes
|
||||
|
||||
/**
|
||||
* Configuration options for the streaming JSON parser
|
||||
*/
|
||||
class StreamParserConfig {
|
||||
constructor(config = {}) {
|
||||
this.jsonPaths = config.jsonPaths;
|
||||
this.onProgress = config.onProgress;
|
||||
this.onError = config.onError;
|
||||
this.estimateTokens =
|
||||
config.estimateTokens || ((text) => Math.ceil(text.length / 4));
|
||||
this.expectedTotal = config.expectedTotal || 0;
|
||||
this.fallbackItemExtractor = config.fallbackItemExtractor;
|
||||
this.itemValidator =
|
||||
config.itemValidator || StreamParserConfig.defaultItemValidator;
|
||||
this.maxBufferSize = config.maxBufferSize || DEFAULT_MAX_BUFFER_SIZE;
|
||||
|
||||
this.validate();
|
||||
}
|
||||
|
||||
validate() {
|
||||
if (!this.jsonPaths || !Array.isArray(this.jsonPaths)) {
|
||||
throw new Error('jsonPaths is required and must be an array');
|
||||
}
|
||||
if (this.jsonPaths.length === 0) {
|
||||
throw new Error('jsonPaths array cannot be empty');
|
||||
}
|
||||
if (this.maxBufferSize <= 0) {
|
||||
throw new Error('maxBufferSize must be positive');
|
||||
}
|
||||
if (this.expectedTotal < 0) {
|
||||
throw new Error('expectedTotal cannot be negative');
|
||||
}
|
||||
if (this.estimateTokens && typeof this.estimateTokens !== 'function') {
|
||||
throw new Error('estimateTokens must be a function');
|
||||
}
|
||||
if (this.onProgress && typeof this.onProgress !== 'function') {
|
||||
throw new Error('onProgress must be a function');
|
||||
}
|
||||
if (this.onError && typeof this.onError !== 'function') {
|
||||
throw new Error('onError must be a function');
|
||||
}
|
||||
if (
|
||||
this.fallbackItemExtractor &&
|
||||
typeof this.fallbackItemExtractor !== 'function'
|
||||
) {
|
||||
throw new Error('fallbackItemExtractor must be a function');
|
||||
}
|
||||
if (this.itemValidator && typeof this.itemValidator !== 'function') {
|
||||
throw new Error('itemValidator must be a function');
|
||||
}
|
||||
}
|
||||
|
||||
static defaultItemValidator(item) {
|
||||
return (
|
||||
item && item.title && typeof item.title === 'string' && item.title.trim()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages progress tracking and metadata
|
||||
*/
|
||||
class ProgressTracker {
|
||||
constructor(config) {
|
||||
this.onProgress = config.onProgress;
|
||||
this.onError = config.onError;
|
||||
this.estimateTokens = config.estimateTokens;
|
||||
this.expectedTotal = config.expectedTotal;
|
||||
this.parsedItems = [];
|
||||
this.accumulatedText = '';
|
||||
}
|
||||
|
||||
addItem(item) {
|
||||
this.parsedItems.push(item);
|
||||
this.reportProgress(item);
|
||||
}
|
||||
|
||||
addText(chunk) {
|
||||
this.accumulatedText += chunk;
|
||||
}
|
||||
|
||||
getMetadata() {
|
||||
return {
|
||||
currentCount: this.parsedItems.length,
|
||||
expectedTotal: this.expectedTotal,
|
||||
accumulatedText: this.accumulatedText,
|
||||
estimatedTokens: this.estimateTokens(this.accumulatedText)
|
||||
};
|
||||
}
|
||||
|
||||
reportProgress(item) {
|
||||
if (!this.onProgress) return;
|
||||
|
||||
try {
|
||||
this.onProgress(item, this.getMetadata());
|
||||
} catch (progressError) {
|
||||
this.handleProgressError(progressError);
|
||||
}
|
||||
}
|
||||
|
||||
handleProgressError(error) {
|
||||
if (this.onError) {
|
||||
this.onError(new Error(`Progress callback failed: ${error.message}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles stream processing with different stream types
|
||||
*/
|
||||
class StreamProcessor {
|
||||
constructor(onChunk) {
|
||||
this.onChunk = onChunk;
|
||||
}
|
||||
|
||||
async process(textStream) {
|
||||
const streamHandler = this.detectStreamType(textStream);
|
||||
await streamHandler(textStream);
|
||||
}
|
||||
|
||||
detectStreamType(textStream) {
|
||||
// Check for textStream property
|
||||
if (this.hasAsyncIterator(textStream?.textStream)) {
|
||||
return (stream) => this.processTextStream(stream.textStream);
|
||||
}
|
||||
|
||||
// Check for fullStream property
|
||||
if (this.hasAsyncIterator(textStream?.fullStream)) {
|
||||
return (stream) => this.processFullStream(stream.fullStream);
|
||||
}
|
||||
|
||||
// Check if stream itself is iterable
|
||||
if (this.hasAsyncIterator(textStream)) {
|
||||
return (stream) => this.processDirectStream(stream);
|
||||
}
|
||||
|
||||
throw new StreamingError(
|
||||
'Stream object is not iterable - no textStream, fullStream, or direct async iterator found',
|
||||
STREAMING_ERROR_CODES.STREAM_NOT_ITERABLE
|
||||
);
|
||||
}
|
||||
|
||||
hasAsyncIterator(obj) {
|
||||
return obj && typeof obj[Symbol.asyncIterator] === 'function';
|
||||
}
|
||||
|
||||
async processTextStream(stream) {
|
||||
for await (const chunk of stream) {
|
||||
this.onChunk(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
async processFullStream(stream) {
|
||||
for await (const chunk of stream) {
|
||||
if (chunk.type === 'text-delta' && chunk.textDelta) {
|
||||
this.onChunk(chunk.textDelta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async processDirectStream(stream) {
|
||||
for await (const chunk of stream) {
|
||||
this.onChunk(chunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages JSON parsing with the streaming parser
|
||||
*/
|
||||
class JSONStreamParser {
|
||||
constructor(config, progressTracker) {
|
||||
this.config = config;
|
||||
this.progressTracker = progressTracker;
|
||||
this.parser = new JSONParser({ paths: config.jsonPaths });
|
||||
this.setupHandlers();
|
||||
}
|
||||
|
||||
setupHandlers() {
|
||||
this.parser.onValue = (value, key, parent, stack) => {
|
||||
this.handleParsedValue(value);
|
||||
};
|
||||
|
||||
this.parser.onError = (error) => {
|
||||
this.handleParseError(error);
|
||||
};
|
||||
}
|
||||
|
||||
handleParsedValue(value) {
|
||||
// Extract the actual item object from the parser's nested structure
|
||||
const item = value.value || value;
|
||||
|
||||
if (this.config.itemValidator(item)) {
|
||||
this.progressTracker.addItem(item);
|
||||
}
|
||||
}
|
||||
|
||||
handleParseError(error) {
|
||||
if (this.config.onError) {
|
||||
this.config.onError(new Error(`JSON parsing error: ${error.message}`));
|
||||
}
|
||||
// Don't throw here - we'll handle this in the fallback logic
|
||||
}
|
||||
|
||||
write(chunk) {
|
||||
this.parser.write(chunk);
|
||||
}
|
||||
|
||||
end() {
|
||||
this.parser.end();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles fallback parsing when streaming fails
|
||||
*/
|
||||
class FallbackParser {
|
||||
constructor(config, progressTracker) {
|
||||
this.config = config;
|
||||
this.progressTracker = progressTracker;
|
||||
}
|
||||
|
||||
async attemptParsing() {
|
||||
if (!this.shouldAttemptFallback()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.parseFallbackItems();
|
||||
} catch (parseError) {
|
||||
this.handleFallbackError(parseError);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
shouldAttemptFallback() {
|
||||
return (
|
||||
this.config.expectedTotal > 0 &&
|
||||
this.progressTracker.parsedItems.length < this.config.expectedTotal &&
|
||||
this.progressTracker.accumulatedText &&
|
||||
this.config.fallbackItemExtractor
|
||||
);
|
||||
}
|
||||
|
||||
async parseFallbackItems() {
|
||||
const jsonText = this._cleanJsonText(this.progressTracker.accumulatedText);
|
||||
const fullResponse = JSON.parse(jsonText);
|
||||
const fallbackItems = this.config.fallbackItemExtractor(fullResponse);
|
||||
|
||||
if (!Array.isArray(fallbackItems)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this._processNewItems(fallbackItems);
|
||||
}
|
||||
|
||||
_cleanJsonText(text) {
|
||||
// Remove markdown code block wrappers and trim whitespace
|
||||
return text
|
||||
.replace(/^```(?:json)?\s*\n?/i, '')
|
||||
.replace(/\n?```\s*$/i, '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
_processNewItems(fallbackItems) {
|
||||
// Only add items we haven't already parsed
|
||||
const itemsToAdd = fallbackItems.slice(
|
||||
this.progressTracker.parsedItems.length
|
||||
);
|
||||
const newItems = [];
|
||||
|
||||
for (const item of itemsToAdd) {
|
||||
if (this.config.itemValidator(item)) {
|
||||
newItems.push(item);
|
||||
this.progressTracker.addItem(item);
|
||||
}
|
||||
}
|
||||
|
||||
return newItems;
|
||||
}
|
||||
|
||||
handleFallbackError(error) {
|
||||
if (this.progressTracker.parsedItems.length === 0) {
|
||||
throw new Error(`Failed to parse AI response as JSON: ${error.message}`);
|
||||
}
|
||||
// If we have some items from streaming, continue with those
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Buffer size validator
|
||||
*/
|
||||
class BufferSizeValidator {
|
||||
constructor(maxSize) {
|
||||
this.maxSize = maxSize;
|
||||
this.currentSize = 0;
|
||||
}
|
||||
|
||||
validateChunk(existingText, newChunk) {
|
||||
const newSize = Buffer.byteLength(existingText + newChunk, 'utf8');
|
||||
|
||||
if (newSize > this.maxSize) {
|
||||
throw new StreamingError(
|
||||
`Buffer size exceeded: ${newSize} bytes > ${this.maxSize} bytes maximum`,
|
||||
STREAMING_ERROR_CODES.BUFFER_SIZE_EXCEEDED
|
||||
);
|
||||
}
|
||||
|
||||
this.currentSize = newSize;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main orchestrator for stream parsing
|
||||
*/
|
||||
class StreamParserOrchestrator {
|
||||
constructor(config) {
|
||||
this.config = new StreamParserConfig(config);
|
||||
this.progressTracker = new ProgressTracker(this.config);
|
||||
this.bufferValidator = new BufferSizeValidator(this.config.maxBufferSize);
|
||||
this.jsonParser = new JSONStreamParser(this.config, this.progressTracker);
|
||||
this.fallbackParser = new FallbackParser(this.config, this.progressTracker);
|
||||
}
|
||||
|
||||
async parse(textStream) {
|
||||
if (!textStream) {
|
||||
throw new Error('No text stream provided');
|
||||
}
|
||||
|
||||
await this.processStream(textStream);
|
||||
await this.waitForParsingCompletion();
|
||||
|
||||
const usedFallback = await this.attemptFallbackIfNeeded();
|
||||
|
||||
return this.buildResult(usedFallback);
|
||||
}
|
||||
|
||||
async processStream(textStream) {
|
||||
const processor = new StreamProcessor((chunk) => {
|
||||
this.bufferValidator.validateChunk(
|
||||
this.progressTracker.accumulatedText,
|
||||
chunk
|
||||
);
|
||||
this.progressTracker.addText(chunk);
|
||||
this.jsonParser.write(chunk);
|
||||
});
|
||||
|
||||
try {
|
||||
await processor.process(textStream);
|
||||
} catch (streamError) {
|
||||
this.handleStreamError(streamError);
|
||||
}
|
||||
|
||||
this.jsonParser.end();
|
||||
}
|
||||
|
||||
handleStreamError(error) {
|
||||
// Re-throw StreamingError as-is, wrap other errors
|
||||
if (error instanceof StreamingError) {
|
||||
throw error;
|
||||
}
|
||||
throw new StreamingError(
|
||||
`Failed to process AI text stream: ${error.message}`,
|
||||
STREAMING_ERROR_CODES.STREAM_PROCESSING_FAILED
|
||||
);
|
||||
}
|
||||
|
||||
async waitForParsingCompletion() {
|
||||
// Wait for final parsing to complete (JSON parser may still be processing)
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
async attemptFallbackIfNeeded() {
|
||||
const fallbackItems = await this.fallbackParser.attemptParsing();
|
||||
return fallbackItems.length > 0;
|
||||
}
|
||||
|
||||
buildResult(usedFallback) {
|
||||
const metadata = this.progressTracker.getMetadata();
|
||||
|
||||
return {
|
||||
items: this.progressTracker.parsedItems,
|
||||
accumulatedText: metadata.accumulatedText,
|
||||
estimatedTokens: metadata.estimatedTokens,
|
||||
usedFallback
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a streaming JSON response with progress tracking
|
||||
*
|
||||
* Example with custom buffer size:
|
||||
* ```js
|
||||
* const result = await parseStream(stream, {
|
||||
* jsonPaths: ['$.tasks.*'],
|
||||
* maxBufferSize: 2 * 1024 * 1024 // 2MB
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @param {Object} textStream - The AI service text stream object
|
||||
* @param {Object} config - Configuration options
|
||||
* @returns {Promise<Object>} Parsed result with metadata
|
||||
*/
|
||||
export async function parseStream(textStream, config = {}) {
|
||||
const orchestrator = new StreamParserOrchestrator(config);
|
||||
return orchestrator.parse(textStream);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process different types of text streams
|
||||
* @param {Object} textStream - The stream object from AI service
|
||||
* @param {Function} onChunk - Callback for each text chunk
|
||||
*/
|
||||
export async function processTextStream(textStream, onChunk) {
|
||||
const processor = new StreamProcessor(onChunk);
|
||||
await processor.process(textStream);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt fallback JSON parsing when streaming parsing is incomplete
|
||||
* @param {string} accumulatedText - Complete accumulated text
|
||||
* @param {Array} existingItems - Items already parsed from streaming
|
||||
* @param {number} expectedTotal - Expected total number of items
|
||||
* @param {Object} config - Configuration for progress reporting
|
||||
* @returns {Promise<Array>} Additional items found via fallback parsing
|
||||
*/
|
||||
export async function attemptFallbackParsing(
|
||||
accumulatedText,
|
||||
existingItems,
|
||||
expectedTotal,
|
||||
config
|
||||
) {
|
||||
// Create a temporary progress tracker for backward compatibility
|
||||
const progressTracker = new ProgressTracker({
|
||||
onProgress: config.onProgress,
|
||||
onError: config.onError,
|
||||
estimateTokens: config.estimateTokens,
|
||||
expectedTotal
|
||||
});
|
||||
|
||||
progressTracker.parsedItems = existingItems;
|
||||
progressTracker.accumulatedText = accumulatedText;
|
||||
|
||||
const fallbackParser = new FallbackParser(
|
||||
{
|
||||
...config,
|
||||
expectedTotal,
|
||||
itemValidator:
|
||||
config.itemValidator || StreamParserConfig.defaultItemValidator
|
||||
},
|
||||
progressTracker
|
||||
);
|
||||
|
||||
return fallbackParser.attemptParsing();
|
||||
}
|
||||
189
src/utils/timeout-manager.js
Normal file
189
src/utils/timeout-manager.js
Normal file
@@ -0,0 +1,189 @@
|
||||
import { StreamingError, STREAMING_ERROR_CODES } from './stream-parser.js';
|
||||
|
||||
/**
|
||||
* Utility class for managing timeouts in async operations
|
||||
* Reduces code duplication for timeout handling patterns
|
||||
*/
|
||||
export class TimeoutManager {
|
||||
/**
|
||||
* Wraps a promise with a timeout that will reject if not resolved in time
|
||||
*
|
||||
* @param {Promise} promise - The promise to wrap with timeout
|
||||
* @param {number} timeoutMs - Timeout duration in milliseconds
|
||||
* @param {string} operationName - Name of the operation for error messages
|
||||
* @returns {Promise} The result of the promise or throws timeout error
|
||||
*
|
||||
* @example
|
||||
* const result = await TimeoutManager.withTimeout(
|
||||
* fetchData(),
|
||||
* 5000,
|
||||
* 'Data fetch operation'
|
||||
* );
|
||||
*/
|
||||
static async withTimeout(promise, timeoutMs, operationName = 'Operation') {
|
||||
let timeoutHandle;
|
||||
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
timeoutHandle = setTimeout(() => {
|
||||
reject(
|
||||
new StreamingError(
|
||||
`${operationName} timed out after ${timeoutMs / 1000} seconds`,
|
||||
STREAMING_ERROR_CODES.STREAM_PROCESSING_FAILED
|
||||
)
|
||||
);
|
||||
}, timeoutMs);
|
||||
});
|
||||
|
||||
try {
|
||||
// Race between the actual promise and the timeout
|
||||
const result = await Promise.race([promise, timeoutPromise]);
|
||||
// Clear timeout if promise resolved first
|
||||
clearTimeout(timeoutHandle);
|
||||
return result;
|
||||
} catch (error) {
|
||||
// Always clear timeout on error
|
||||
clearTimeout(timeoutHandle);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a promise with a timeout, but returns undefined instead of throwing on timeout
|
||||
* Useful for optional operations that shouldn't fail the main flow
|
||||
*
|
||||
* @param {Promise} promise - The promise to wrap with timeout
|
||||
* @param {number} timeoutMs - Timeout duration in milliseconds
|
||||
* @param {*} defaultValue - Value to return on timeout (default: undefined)
|
||||
* @returns {Promise} The result of the promise or defaultValue on timeout
|
||||
*
|
||||
* @example
|
||||
* const usage = await TimeoutManager.withSoftTimeout(
|
||||
* getUsageStats(),
|
||||
* 1000,
|
||||
* { tokens: 0 }
|
||||
* );
|
||||
*/
|
||||
static async withSoftTimeout(promise, timeoutMs, defaultValue = undefined) {
|
||||
let timeoutHandle;
|
||||
|
||||
const timeoutPromise = new Promise((resolve) => {
|
||||
timeoutHandle = setTimeout(() => {
|
||||
resolve(defaultValue);
|
||||
}, timeoutMs);
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await Promise.race([promise, timeoutPromise]);
|
||||
clearTimeout(timeoutHandle);
|
||||
return result;
|
||||
} catch (error) {
|
||||
// On error, clear timeout and return default value
|
||||
clearTimeout(timeoutHandle);
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a reusable timeout controller for multiple operations
|
||||
* Useful when you need to apply the same timeout to multiple promises
|
||||
*
|
||||
* @param {number} timeoutMs - Timeout duration in milliseconds
|
||||
* @param {string} operationName - Base name for operations
|
||||
* @returns {Object} Controller with wrap method
|
||||
*
|
||||
* @example
|
||||
* const controller = TimeoutManager.createController(60000, 'AI Service');
|
||||
* const result1 = await controller.wrap(service.call1(), 'call 1');
|
||||
* const result2 = await controller.wrap(service.call2(), 'call 2');
|
||||
*/
|
||||
static createController(timeoutMs, operationName = 'Operation') {
|
||||
return {
|
||||
timeoutMs,
|
||||
operationName,
|
||||
|
||||
async wrap(promise, specificName = null) {
|
||||
const fullName = specificName
|
||||
? `${operationName} - ${specificName}`
|
||||
: operationName;
|
||||
return TimeoutManager.withTimeout(promise, timeoutMs, fullName);
|
||||
},
|
||||
|
||||
async wrapSoft(promise, defaultValue = undefined) {
|
||||
return TimeoutManager.withSoftTimeout(promise, timeoutMs, defaultValue);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an error is a timeout error from this manager
|
||||
*
|
||||
* @param {Error} error - The error to check
|
||||
* @returns {boolean} True if this is a timeout error
|
||||
*/
|
||||
static isTimeoutError(error) {
|
||||
return (
|
||||
error instanceof StreamingError &&
|
||||
error.code === STREAMING_ERROR_CODES.STREAM_PROCESSING_FAILED &&
|
||||
error.message.includes('timed out')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Duration helper class for more readable timeout specifications
|
||||
*/
|
||||
export class Duration {
|
||||
constructor(value, unit = 'ms') {
|
||||
this.milliseconds = this._toMilliseconds(value, unit);
|
||||
}
|
||||
|
||||
static milliseconds(value) {
|
||||
return new Duration(value, 'ms');
|
||||
}
|
||||
|
||||
static seconds(value) {
|
||||
return new Duration(value, 's');
|
||||
}
|
||||
|
||||
static minutes(value) {
|
||||
return new Duration(value, 'm');
|
||||
}
|
||||
|
||||
static hours(value) {
|
||||
return new Duration(value, 'h');
|
||||
}
|
||||
|
||||
get seconds() {
|
||||
return this.milliseconds / 1000;
|
||||
}
|
||||
|
||||
get minutes() {
|
||||
return this.milliseconds / 60000;
|
||||
}
|
||||
|
||||
get hours() {
|
||||
return this.milliseconds / 3600000;
|
||||
}
|
||||
|
||||
toString() {
|
||||
if (this.milliseconds < 1000) {
|
||||
return `${this.milliseconds}ms`;
|
||||
} else if (this.milliseconds < 60000) {
|
||||
return `${this.seconds}s`;
|
||||
} else if (this.milliseconds < 3600000) {
|
||||
return `${Math.floor(this.minutes)}m ${Math.floor(this.seconds % 60)}s`;
|
||||
} else {
|
||||
return `${Math.floor(this.hours)}h ${Math.floor(this.minutes % 60)}m`;
|
||||
}
|
||||
}
|
||||
|
||||
_toMilliseconds(value, unit) {
|
||||
const conversions = {
|
||||
ms: 1,
|
||||
s: 1000,
|
||||
m: 60000,
|
||||
h: 3600000
|
||||
};
|
||||
return value * (conversions[unit] || 1);
|
||||
}
|
||||
}
|
||||
@@ -158,20 +158,25 @@ describe('Cross-Tag Move CLI Integration', () => {
|
||||
);
|
||||
|
||||
try {
|
||||
await moveTaskModule.moveTasksBetweenTags(
|
||||
const result = await moveTaskModule.moveTasksBetweenTags(
|
||||
tasksPath,
|
||||
taskIds,
|
||||
sourceTag,
|
||||
toTag,
|
||||
{
|
||||
withDependencies,
|
||||
ignoreDependencies,
|
||||
force
|
||||
ignoreDependencies
|
||||
}
|
||||
);
|
||||
|
||||
console.log(chalk.green('Successfully moved task(s) between tags'));
|
||||
|
||||
// Print advisory tips when present
|
||||
if (result && Array.isArray(result.tips) && result.tips.length > 0) {
|
||||
console.log('Next Steps:');
|
||||
result.tips.forEach((t) => console.log(` • ${t}`));
|
||||
}
|
||||
|
||||
// Generate task files for both tags
|
||||
await generateTaskFilesModule.default(
|
||||
tasksPath,
|
||||
@@ -185,6 +190,21 @@ describe('Cross-Tag Move CLI Integration', () => {
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Error: ${error.message}`));
|
||||
// Print ID collision guidance similar to CLI help block
|
||||
if (
|
||||
typeof error?.message === 'string' &&
|
||||
error.message.includes('already exists in target tag')
|
||||
) {
|
||||
console.log('');
|
||||
console.log('Conflict: ID already exists in target tag');
|
||||
console.log(
|
||||
' • Choose a different target tag without conflicting IDs'
|
||||
);
|
||||
console.log(' • Move a different set of IDs (avoid existing ones)');
|
||||
console.log(
|
||||
' • If needed, move within-tag to a new ID first, then cross-tag move'
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
@@ -268,8 +288,7 @@ describe('Cross-Tag Move CLI Integration', () => {
|
||||
'in-progress',
|
||||
{
|
||||
withDependencies: undefined,
|
||||
ignoreDependencies: undefined,
|
||||
force: undefined
|
||||
ignoreDependencies: undefined
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -323,8 +342,7 @@ describe('Cross-Tag Move CLI Integration', () => {
|
||||
'in-progress',
|
||||
{
|
||||
withDependencies: true,
|
||||
ignoreDependencies: undefined,
|
||||
force: undefined
|
||||
ignoreDependencies: undefined
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -350,8 +368,7 @@ describe('Cross-Tag Move CLI Integration', () => {
|
||||
'in-progress',
|
||||
{
|
||||
withDependencies: undefined,
|
||||
ignoreDependencies: true,
|
||||
force: undefined
|
||||
ignoreDependencies: true
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -376,8 +393,7 @@ describe('Cross-Tag Move CLI Integration', () => {
|
||||
'new-tag',
|
||||
{
|
||||
withDependencies: undefined,
|
||||
ignoreDependencies: undefined,
|
||||
force: undefined
|
||||
ignoreDependencies: undefined
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -440,6 +456,57 @@ describe('Cross-Tag Move CLI Integration', () => {
|
||||
restore();
|
||||
});
|
||||
|
||||
it('should print advisory tips when result.tips are returned (ignore-dependencies)', async () => {
|
||||
const { errorMessages, logMessages, restore } = captureConsoleAndExit();
|
||||
try {
|
||||
// Arrange: mock move to return tips
|
||||
mockMoveTasksBetweenTags.mockResolvedValue({
|
||||
message: 'ok',
|
||||
tips: [
|
||||
'Run "task-master validate-dependencies" to check for dependency issues.',
|
||||
'Run "task-master fix-dependencies" to automatically repair dangling dependencies.'
|
||||
]
|
||||
});
|
||||
|
||||
await moveAction({
|
||||
from: '2',
|
||||
fromTag: 'backlog',
|
||||
toTag: 'in-progress',
|
||||
ignoreDependencies: true
|
||||
});
|
||||
|
||||
const joined = logMessages.join('\n');
|
||||
expect(joined).toContain('Next Steps');
|
||||
expect(joined).toContain('validate-dependencies');
|
||||
expect(joined).toContain('fix-dependencies');
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
|
||||
it('should print ID collision suggestions when target already has the ID', async () => {
|
||||
const { errorMessages, logMessages, restore } = captureConsoleAndExit();
|
||||
try {
|
||||
// Arrange: mock move to throw collision
|
||||
const err = new Error(
|
||||
'Task 1 already exists in target tag "in-progress"'
|
||||
);
|
||||
mockMoveTasksBetweenTags.mockRejectedValue(err);
|
||||
|
||||
await expect(
|
||||
moveAction({ from: '1', fromTag: 'backlog', toTag: 'in-progress' })
|
||||
).rejects.toThrow('already exists in target tag');
|
||||
|
||||
const joined = logMessages.join('\n');
|
||||
expect(joined).toContain('Conflict: ID already exists in target tag');
|
||||
expect(joined).toContain('different target tag');
|
||||
expect(joined).toContain('different set of IDs');
|
||||
expect(joined).toContain('within-tag');
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle same tag error correctly', async () => {
|
||||
const options = {
|
||||
from: '1',
|
||||
@@ -485,8 +552,7 @@ describe('Cross-Tag Move CLI Integration', () => {
|
||||
from: '1',
|
||||
toTag: 'in-progress',
|
||||
withDependencies: false,
|
||||
ignoreDependencies: false,
|
||||
force: false
|
||||
ignoreDependencies: false
|
||||
// fromTag is intentionally not provided to test fallback
|
||||
});
|
||||
|
||||
@@ -498,8 +564,7 @@ describe('Cross-Tag Move CLI Integration', () => {
|
||||
'in-progress',
|
||||
{
|
||||
withDependencies: false,
|
||||
ignoreDependencies: false,
|
||||
force: false
|
||||
ignoreDependencies: false
|
||||
}
|
||||
);
|
||||
|
||||
@@ -536,8 +601,7 @@ describe('Cross-Tag Move CLI Integration', () => {
|
||||
'in-progress',
|
||||
{
|
||||
withDependencies: undefined,
|
||||
ignoreDependencies: undefined,
|
||||
force: undefined
|
||||
ignoreDependencies: undefined
|
||||
}
|
||||
);
|
||||
|
||||
@@ -555,32 +619,7 @@ describe('Cross-Tag Move CLI Integration', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle --force flag correctly', async () => {
|
||||
// Mock successful cross-tag move with force flag
|
||||
mockMoveTasksBetweenTags.mockResolvedValue(undefined);
|
||||
mockGenerateTaskFiles.mockResolvedValue(undefined);
|
||||
|
||||
const options = {
|
||||
from: '1',
|
||||
fromTag: 'backlog',
|
||||
toTag: 'in-progress',
|
||||
force: true
|
||||
};
|
||||
|
||||
await moveAction(options);
|
||||
|
||||
expect(mockMoveTasksBetweenTags).toHaveBeenCalledWith(
|
||||
expect.stringContaining('tasks.json'),
|
||||
[1],
|
||||
'backlog',
|
||||
'in-progress',
|
||||
{
|
||||
withDependencies: undefined,
|
||||
ignoreDependencies: undefined,
|
||||
force: true // Force flag should be passed through
|
||||
}
|
||||
);
|
||||
});
|
||||
// Note: --force flag is no longer supported for cross-tag moves
|
||||
|
||||
it('should fail when invalid task ID is provided', async () => {
|
||||
const options = {
|
||||
@@ -662,90 +701,11 @@ describe('Cross-Tag Move CLI Integration', () => {
|
||||
restore();
|
||||
});
|
||||
|
||||
it('should combine --with-dependencies and --force flags correctly', async () => {
|
||||
// Mock successful cross-tag move with both flags
|
||||
mockMoveTasksBetweenTags.mockResolvedValue(undefined);
|
||||
mockGenerateTaskFiles.mockResolvedValue(undefined);
|
||||
// Note: --force combinations removed
|
||||
|
||||
const options = {
|
||||
from: '1,2',
|
||||
fromTag: 'backlog',
|
||||
toTag: 'in-progress',
|
||||
withDependencies: true,
|
||||
force: true
|
||||
};
|
||||
// Note: --force combinations removed
|
||||
|
||||
await moveAction(options);
|
||||
|
||||
expect(mockMoveTasksBetweenTags).toHaveBeenCalledWith(
|
||||
expect.stringContaining('tasks.json'),
|
||||
[1, 2],
|
||||
'backlog',
|
||||
'in-progress',
|
||||
{
|
||||
withDependencies: true,
|
||||
ignoreDependencies: undefined,
|
||||
force: true // Both flags should be passed
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should combine --ignore-dependencies and --force flags correctly', async () => {
|
||||
// Mock successful cross-tag move with both flags
|
||||
mockMoveTasksBetweenTags.mockResolvedValue(undefined);
|
||||
mockGenerateTaskFiles.mockResolvedValue(undefined);
|
||||
|
||||
const options = {
|
||||
from: '1',
|
||||
fromTag: 'backlog',
|
||||
toTag: 'in-progress',
|
||||
ignoreDependencies: true,
|
||||
force: true
|
||||
};
|
||||
|
||||
await moveAction(options);
|
||||
|
||||
expect(mockMoveTasksBetweenTags).toHaveBeenCalledWith(
|
||||
expect.stringContaining('tasks.json'),
|
||||
[1],
|
||||
'backlog',
|
||||
'in-progress',
|
||||
{
|
||||
withDependencies: undefined,
|
||||
ignoreDependencies: true,
|
||||
force: true // Both flags should be passed
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle all three flags combined correctly', async () => {
|
||||
// Mock successful cross-tag move with all flags
|
||||
mockMoveTasksBetweenTags.mockResolvedValue(undefined);
|
||||
mockGenerateTaskFiles.mockResolvedValue(undefined);
|
||||
|
||||
const options = {
|
||||
from: '1,2,3',
|
||||
fromTag: 'backlog',
|
||||
toTag: 'in-progress',
|
||||
withDependencies: true,
|
||||
ignoreDependencies: true,
|
||||
force: true
|
||||
};
|
||||
|
||||
await moveAction(options);
|
||||
|
||||
expect(mockMoveTasksBetweenTags).toHaveBeenCalledWith(
|
||||
expect.stringContaining('tasks.json'),
|
||||
[1, 2, 3],
|
||||
'backlog',
|
||||
'in-progress',
|
||||
{
|
||||
withDependencies: true,
|
||||
ignoreDependencies: true,
|
||||
force: true // All three flags should be passed
|
||||
}
|
||||
);
|
||||
});
|
||||
// Note: --force combinations removed
|
||||
|
||||
it('should handle whitespace in comma-separated task IDs', async () => {
|
||||
// Mock successful cross-tag move with whitespace
|
||||
|
||||
@@ -426,6 +426,38 @@ describe('Cross-Tag Task Movement Integration Tests', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should provide advisory tips when ignoreDependencies breaks deps', async () => {
|
||||
// Move a task that has dependencies so cross-tag conflicts would be broken
|
||||
const taskIds = [2]; // backlog:2 depends on 1
|
||||
const sourceTag = 'backlog';
|
||||
const targetTag = 'in-progress';
|
||||
|
||||
// Override cross-tag detection to simulate conflicts for this case
|
||||
const depManager = await import(
|
||||
'../../scripts/modules/dependency-manager.js'
|
||||
);
|
||||
depManager.findCrossTagDependencies.mockReturnValueOnce([
|
||||
{ taskId: 2, dependencyId: 1, dependencyTag: sourceTag }
|
||||
]);
|
||||
|
||||
const result = await moveTasksBetweenTags(
|
||||
testDataPath,
|
||||
taskIds,
|
||||
sourceTag,
|
||||
targetTag,
|
||||
{ ignoreDependencies: true },
|
||||
{ projectRoot: '/test/project' }
|
||||
);
|
||||
|
||||
expect(Array.isArray(result.tips)).toBe(true);
|
||||
const expectedTips = [
|
||||
'Run "task-master validate-dependencies" to check for dependency issues.',
|
||||
'Run "task-master fix-dependencies" to automatically repair dangling dependencies.'
|
||||
];
|
||||
expect(result.tips).toHaveLength(expectedTips.length);
|
||||
expect(result.tips).toEqual(expect.arrayContaining(expectedTips));
|
||||
});
|
||||
|
||||
it('should move task without cross-tag dependency conflicts (since dependencies only exist within tags)', async () => {
|
||||
const taskIds = [2]; // Task 2 depends on Task 1 (both in same tag)
|
||||
const sourceTag = 'backlog';
|
||||
@@ -564,6 +596,25 @@ describe('Cross-Tag Task Movement Integration Tests', () => {
|
||||
{ projectRoot: '/test/project' }
|
||||
)
|
||||
).rejects.toThrow('Task 1 already exists in target tag "in-progress"');
|
||||
|
||||
// Validate suggestions on the error payload
|
||||
try {
|
||||
await moveTasksBetweenTags(
|
||||
testDataPath,
|
||||
taskIds,
|
||||
sourceTag,
|
||||
targetTag,
|
||||
{},
|
||||
{ projectRoot: '/test/project' }
|
||||
);
|
||||
} catch (err) {
|
||||
expect(err.code).toBe('TASK_ALREADY_EXISTS');
|
||||
expect(Array.isArray(err.data?.suggestions)).toBe(true);
|
||||
const s = (err.data?.suggestions || []).join(' ');
|
||||
expect(s).toContain('different target tag');
|
||||
expect(s).toContain('different set of IDs');
|
||||
expect(s).toContain('within-tag');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -637,37 +688,7 @@ describe('Cross-Tag Task Movement Integration Tests', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle force flag for dependency conflicts', async () => {
|
||||
const taskIds = [2]; // Task 2 depends on Task 1
|
||||
const sourceTag = 'backlog';
|
||||
const targetTag = 'in-progress';
|
||||
|
||||
const result = await moveTasksBetweenTags(
|
||||
testDataPath,
|
||||
taskIds,
|
||||
sourceTag,
|
||||
targetTag,
|
||||
{ force: true },
|
||||
{ projectRoot: '/test/project' }
|
||||
);
|
||||
|
||||
// Verify task was moved despite dependency conflicts
|
||||
expect(mockUtils.writeJSON).toHaveBeenCalledWith(
|
||||
testDataPath,
|
||||
expect.objectContaining({
|
||||
'in-progress': expect.objectContaining({
|
||||
tasks: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: 2,
|
||||
tag: 'in-progress'
|
||||
})
|
||||
])
|
||||
})
|
||||
}),
|
||||
'/test/project',
|
||||
null
|
||||
);
|
||||
});
|
||||
// Note: force flag deprecated for cross-tag moves; covered by with/ignore dependencies tests
|
||||
});
|
||||
|
||||
describe('Complex Scenarios', () => {
|
||||
|
||||
97
tests/manual/progress/TESTING_GUIDE.md
Normal file
97
tests/manual/progress/TESTING_GUIDE.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# Task Master Progress Testing Guide
|
||||
|
||||
Quick reference for testing streaming/non-streaming functionality with token tracking.
|
||||
|
||||
## 🎯 Test Modes
|
||||
|
||||
1. **MCP Streaming** - Has `reportProgress` + `mcpLog`, shows emoji indicators (🔴🟠🟢)
|
||||
2. **CLI Streaming** - No `reportProgress`, shows terminal progress bars
|
||||
3. **Non-Streaming** - No progress reporting, single response
|
||||
|
||||
## 🚀 Quick Commands
|
||||
|
||||
```bash
|
||||
# Test Scripts (accept: mcp-streaming, cli-streaming, non-streaming, both, all)
|
||||
node test-parse-prd.js [mode]
|
||||
node test-analyze-complexity.js [mode]
|
||||
node test-expand.js [mode] [num_subtasks]
|
||||
node test-expand-all.js [mode] [num_subtasks]
|
||||
node parse-prd-analysis.js [accuracy|complexity|all]
|
||||
|
||||
# CLI Commands
|
||||
node scripts/dev.js parse-prd test.txt # Local dev (streaming)
|
||||
node scripts/dev.js analyze-complexity --research
|
||||
node scripts/dev.js expand --id=1 --force
|
||||
node scripts/dev.js expand --all --force
|
||||
|
||||
task-master [command] # Global CLI (non-streaming)
|
||||
```
|
||||
|
||||
## ✅ Success Indicators
|
||||
|
||||
### Indicators
|
||||
- **Priority**: 🔴🔴🔴 (high), 🟠🟠⚪ (medium), 🟢⚪⚪ (low)
|
||||
- **Complexity**: ●●● (7-10), ●●○ (4-6), ●○○ (1-3)
|
||||
|
||||
### Token Format
|
||||
`Tokens (I/O): 2,150/1,847 ($0.0423)` (~4 chars per token)
|
||||
|
||||
### Progress Bars
|
||||
```
|
||||
Single: Generating subtasks... |████████░░| 80% (4/5)
|
||||
Dual: Expanding 3 tasks | Task 2/3 |████████░░| 66%
|
||||
Generating 5 subtasks... |██████░░░░| 60%
|
||||
```
|
||||
|
||||
### Fractional Progress
|
||||
`(completedTasks + currentSubtask/totalSubtasks) / totalTasks`
|
||||
Example: 33% → 46% → 60% → 66% → 80% → 93% → 100%
|
||||
|
||||
## 🐛 Quick Fixes
|
||||
|
||||
| Issue | Fix |
|
||||
|-------|-----|
|
||||
| No streaming | Check `reportProgress` is passed |
|
||||
| NaN% progress | Filter duplicate `subtask_progress` events |
|
||||
| Missing tokens | Check `.env` has API keys |
|
||||
| Broken bars | Terminal width > 80 |
|
||||
| projectRoot.split | Use `projectRoot` not `session` |
|
||||
|
||||
```bash
|
||||
# Debug
|
||||
TASKMASTER_DEBUG=true node test-expand.js
|
||||
npm run lint
|
||||
```
|
||||
|
||||
## 📊 Benchmarks
|
||||
- Single task: 10-20s (5 subtasks)
|
||||
- Expand all: 30-45s (3 tasks)
|
||||
- Streaming: ~10-20% faster
|
||||
- Updates: Every 2-5s
|
||||
|
||||
## 🔄 Test Workflow
|
||||
|
||||
```bash
|
||||
# Quick check
|
||||
node test-parse-prd.js both && npm test
|
||||
|
||||
# Full suite (before release)
|
||||
for test in parse-prd analyze-complexity expand expand-all; do
|
||||
node test-$test.js all
|
||||
done
|
||||
node parse-prd-analysis.js all
|
||||
npm test
|
||||
```
|
||||
|
||||
## 🎯 MCP Tool Example
|
||||
|
||||
```javascript
|
||||
{
|
||||
"tool": "parse_prd",
|
||||
"args": {
|
||||
"input": "prd.txt",
|
||||
"numTasks": "8",
|
||||
"force": true,
|
||||
"projectRoot": "/path/to/project"
|
||||
}
|
||||
}
|
||||
334
tests/manual/progress/parse-prd-analysis.js
Normal file
334
tests/manual/progress/parse-prd-analysis.js
Normal file
@@ -0,0 +1,334 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* parse-prd-analysis.js
|
||||
*
|
||||
* Detailed timing and accuracy analysis for parse-prd progress reporting.
|
||||
* Tests different task generation complexities using the sample PRD from fixtures.
|
||||
* Validates real-time characteristics and focuses on progress behavior and performance metrics.
|
||||
* Uses tests/fixtures/sample-prd.txt for consistent testing across all scenarios.
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import chalk from 'chalk';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
import parsePRD from '../../../scripts/modules/task-manager/parse-prd/index.js';
|
||||
|
||||
// Use the same project root as the main test file
|
||||
const PROJECT_ROOT = path.resolve(__dirname, '..', '..', '..');
|
||||
|
||||
/**
|
||||
* Get the path to the sample PRD file
|
||||
*/
|
||||
function getSamplePRDPath() {
|
||||
return path.resolve(PROJECT_ROOT, 'tests', 'fixtures', 'sample-prd.txt');
|
||||
}
|
||||
|
||||
/**
|
||||
* Detailed Progress Reporter for timing analysis
|
||||
*/
|
||||
class DetailedProgressReporter {
|
||||
constructor() {
|
||||
this.progressHistory = [];
|
||||
this.startTime = Date.now();
|
||||
this.lastProgress = 0;
|
||||
}
|
||||
|
||||
async reportProgress(data) {
|
||||
const timestamp = Date.now() - this.startTime;
|
||||
const timeSinceLastProgress =
|
||||
this.progressHistory.length > 0
|
||||
? timestamp -
|
||||
this.progressHistory[this.progressHistory.length - 1].timestamp
|
||||
: timestamp;
|
||||
|
||||
const entry = {
|
||||
timestamp,
|
||||
timeSinceLastProgress,
|
||||
...data
|
||||
};
|
||||
|
||||
this.progressHistory.push(entry);
|
||||
|
||||
const percentage = data.total
|
||||
? Math.round((data.progress / data.total) * 100)
|
||||
: 0;
|
||||
console.log(
|
||||
chalk.blue(`[${timestamp}ms] (+${timeSinceLastProgress}ms)`),
|
||||
chalk.green(`${percentage}%`),
|
||||
`(${data.progress}/${data.total})`,
|
||||
chalk.yellow(data.message)
|
||||
);
|
||||
}
|
||||
|
||||
getAnalysis() {
|
||||
if (this.progressHistory.length === 0) return null;
|
||||
|
||||
const totalDuration =
|
||||
this.progressHistory[this.progressHistory.length - 1].timestamp;
|
||||
const intervals = this.progressHistory
|
||||
.slice(1)
|
||||
.map((entry) => entry.timeSinceLastProgress);
|
||||
const avgInterval =
|
||||
intervals.length > 0
|
||||
? intervals.reduce((a, b) => a + b, 0) / intervals.length
|
||||
: 0;
|
||||
const minInterval = intervals.length > 0 ? Math.min(...intervals) : 0;
|
||||
const maxInterval = intervals.length > 0 ? Math.max(...intervals) : 0;
|
||||
|
||||
return {
|
||||
totalReports: this.progressHistory.length,
|
||||
totalDuration,
|
||||
avgInterval: Math.round(avgInterval),
|
||||
minInterval,
|
||||
maxInterval,
|
||||
intervals
|
||||
};
|
||||
}
|
||||
|
||||
printDetailedAnalysis() {
|
||||
const analysis = this.getAnalysis();
|
||||
if (!analysis) {
|
||||
console.log(chalk.red('No progress data to analyze'));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.cyan('\n=== Detailed Progress Analysis ==='));
|
||||
console.log(`Total Progress Reports: ${analysis.totalReports}`);
|
||||
console.log(`Total Duration: ${analysis.totalDuration}ms`);
|
||||
console.log(`Average Interval: ${analysis.avgInterval}ms`);
|
||||
console.log(`Min Interval: ${analysis.minInterval}ms`);
|
||||
console.log(`Max Interval: ${analysis.maxInterval}ms`);
|
||||
|
||||
console.log(chalk.cyan('\n=== Progress Timeline ==='));
|
||||
this.progressHistory.forEach((entry, index) => {
|
||||
const percentage = entry.total
|
||||
? Math.round((entry.progress / entry.total) * 100)
|
||||
: 0;
|
||||
const intervalText =
|
||||
index > 0 ? ` (+${entry.timeSinceLastProgress}ms)` : '';
|
||||
console.log(
|
||||
`${index + 1}. [${entry.timestamp}ms]${intervalText} ${percentage}% - ${entry.message}`
|
||||
);
|
||||
});
|
||||
|
||||
// Check for real-time characteristics
|
||||
console.log(chalk.cyan('\n=== Real-time Characteristics ==='));
|
||||
const hasRealTimeUpdates = analysis.intervals.some(
|
||||
(interval) => interval < 10000
|
||||
); // Less than 10s
|
||||
const hasConsistentUpdates = analysis.intervals.length > 3;
|
||||
const hasProgressiveUpdates = this.progressHistory.every(
|
||||
(entry, index) =>
|
||||
index === 0 ||
|
||||
entry.progress >= this.progressHistory[index - 1].progress
|
||||
);
|
||||
|
||||
console.log(`✅ Real-time updates: ${hasRealTimeUpdates ? 'YES' : 'NO'}`);
|
||||
console.log(
|
||||
`✅ Consistent updates: ${hasConsistentUpdates ? 'YES' : 'NO'}`
|
||||
);
|
||||
console.log(
|
||||
`✅ Progressive updates: ${hasProgressiveUpdates ? 'YES' : 'NO'}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get PRD path for complexity testing
|
||||
* For complexity testing, we'll use the same sample PRD but request different numbers of tasks
|
||||
* This provides more realistic testing since the AI will generate different complexity based on task count
|
||||
*/
|
||||
function getPRDPathForComplexity(complexity = 'medium') {
|
||||
// Always use the same sample PRD file - complexity will be controlled by task count
|
||||
return getSamplePRDPath();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test streaming with different task generation complexities
|
||||
* Uses the same sample PRD but requests different numbers of tasks to test complexity scaling
|
||||
*/
|
||||
async function testStreamingComplexity() {
|
||||
console.log(
|
||||
chalk.cyan(
|
||||
'🧪 Testing Streaming with Different Task Generation Complexities\n'
|
||||
)
|
||||
);
|
||||
|
||||
const complexities = ['simple', 'medium', 'complex'];
|
||||
const results = [];
|
||||
|
||||
for (const complexity of complexities) {
|
||||
console.log(
|
||||
chalk.yellow(`\n--- Testing ${complexity.toUpperCase()} Complexity ---`)
|
||||
);
|
||||
|
||||
const testPRDPath = getPRDPathForComplexity(complexity);
|
||||
const testTasksPath = path.join(__dirname, `test-tasks-${complexity}.json`);
|
||||
|
||||
// Clean up existing file
|
||||
if (fs.existsSync(testTasksPath)) {
|
||||
fs.unlinkSync(testTasksPath);
|
||||
}
|
||||
|
||||
const progressReporter = new DetailedProgressReporter();
|
||||
const expectedTasks =
|
||||
complexity === 'simple' ? 3 : complexity === 'medium' ? 6 : 10;
|
||||
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
|
||||
await parsePRD(testPRDPath, testTasksPath, expectedTasks, {
|
||||
force: true,
|
||||
append: false,
|
||||
research: false,
|
||||
reportProgress: progressReporter.reportProgress.bind(progressReporter),
|
||||
projectRoot: PROJECT_ROOT
|
||||
});
|
||||
|
||||
const endTime = Date.now();
|
||||
const duration = endTime - startTime;
|
||||
|
||||
console.log(
|
||||
chalk.green(`✅ ${complexity} complexity completed in ${duration}ms`)
|
||||
);
|
||||
|
||||
progressReporter.printDetailedAnalysis();
|
||||
|
||||
results.push({
|
||||
complexity,
|
||||
duration,
|
||||
analysis: progressReporter.getAnalysis()
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(
|
||||
chalk.red(`❌ ${complexity} complexity failed: ${error.message}`)
|
||||
);
|
||||
results.push({
|
||||
complexity,
|
||||
error: error.message
|
||||
});
|
||||
} finally {
|
||||
// Clean up (only the tasks file, not the PRD since we're using the fixture)
|
||||
if (fs.existsSync(testTasksPath)) fs.unlinkSync(testTasksPath);
|
||||
}
|
||||
}
|
||||
|
||||
// Summary
|
||||
console.log(chalk.cyan('\n=== Complexity Test Summary ==='));
|
||||
results.forEach((result) => {
|
||||
if (result.error) {
|
||||
console.log(`${result.complexity}: ❌ FAILED - ${result.error}`);
|
||||
} else {
|
||||
console.log(
|
||||
`${result.complexity}: ✅ ${result.duration}ms (${result.analysis.totalReports} reports)`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test progress accuracy
|
||||
*/
|
||||
async function testProgressAccuracy() {
|
||||
console.log(chalk.cyan('🧪 Testing Progress Accuracy\n'));
|
||||
|
||||
const testPRDPath = getSamplePRDPath();
|
||||
const testTasksPath = path.join(__dirname, 'test-accuracy-tasks.json');
|
||||
|
||||
// Clean up existing file
|
||||
if (fs.existsSync(testTasksPath)) {
|
||||
fs.unlinkSync(testTasksPath);
|
||||
}
|
||||
|
||||
const progressReporter = new DetailedProgressReporter();
|
||||
|
||||
try {
|
||||
await parsePRD(testPRDPath, testTasksPath, 8, {
|
||||
force: true,
|
||||
append: false,
|
||||
research: false,
|
||||
reportProgress: progressReporter.reportProgress.bind(progressReporter),
|
||||
projectRoot: PROJECT_ROOT
|
||||
});
|
||||
|
||||
console.log(chalk.green('✅ Progress accuracy test completed'));
|
||||
progressReporter.printDetailedAnalysis();
|
||||
|
||||
// Additional accuracy checks
|
||||
const analysis = progressReporter.getAnalysis();
|
||||
console.log(chalk.cyan('\n=== Accuracy Metrics ==='));
|
||||
console.log(
|
||||
`Progress consistency: ${analysis.intervals.every((i) => i > 0) ? 'PASS' : 'FAIL'}`
|
||||
);
|
||||
console.log(
|
||||
`Reasonable intervals: ${analysis.intervals.every((i) => i < 30000) ? 'PASS' : 'FAIL'}`
|
||||
);
|
||||
console.log(
|
||||
`Expected report count: ${analysis.totalReports >= 8 ? 'PASS' : 'FAIL'}`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
chalk.red(`❌ Progress accuracy test failed: ${error.message}`)
|
||||
);
|
||||
} finally {
|
||||
// Clean up (only the tasks file, not the PRD since we're using the fixture)
|
||||
if (fs.existsSync(testTasksPath)) fs.unlinkSync(testTasksPath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main test runner
|
||||
*/
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const testType = args[0] || 'accuracy';
|
||||
|
||||
console.log(chalk.bold.cyan('🚀 Task Master Detailed Progress Tests\n'));
|
||||
console.log(chalk.blue(`Test type: ${testType}\n`));
|
||||
|
||||
try {
|
||||
switch (testType.toLowerCase()) {
|
||||
case 'accuracy':
|
||||
await testProgressAccuracy();
|
||||
break;
|
||||
|
||||
case 'complexity':
|
||||
await testStreamingComplexity();
|
||||
break;
|
||||
|
||||
case 'all':
|
||||
console.log(chalk.yellow('Running all detailed tests...\n'));
|
||||
await testProgressAccuracy();
|
||||
console.log('\n' + '='.repeat(60) + '\n');
|
||||
await testStreamingComplexity();
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log(chalk.red(`Unknown test type: ${testType}`));
|
||||
console.log(
|
||||
chalk.yellow('Available options: accuracy, complexity, all')
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(chalk.green('\n🎉 Detailed tests completed successfully!'));
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`\n❌ Test failed: ${error.message}`));
|
||||
console.error(chalk.red(error.stack));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
// Top-level await is available in ESM; keep compatibility with Node ≥14
|
||||
await main();
|
||||
}
|
||||
577
tests/manual/progress/test-parse-prd.js
Normal file
577
tests/manual/progress/test-parse-prd.js
Normal file
@@ -0,0 +1,577 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* test-parse-prd.js
|
||||
*
|
||||
* Comprehensive integration test for parse-prd functionality.
|
||||
* Tests MCP streaming, CLI streaming, and non-streaming modes.
|
||||
* Validates token tracking, message formats, and priority indicators across all contexts.
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import chalk from 'chalk';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
// Get current directory
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// Get project root (three levels up from tests/manual/progress/)
|
||||
const PROJECT_ROOT = path.resolve(__dirname, '..', '..', '..');
|
||||
|
||||
// Import the parse-prd function
|
||||
import parsePRD from '../../../scripts/modules/task-manager/parse-prd/index.js';
|
||||
|
||||
/**
|
||||
* Mock Progress Reporter for testing
|
||||
*/
|
||||
class MockProgressReporter {
|
||||
constructor(enableDebug = true) {
|
||||
this.enableDebug = enableDebug;
|
||||
this.progressHistory = [];
|
||||
this.startTime = Date.now();
|
||||
}
|
||||
|
||||
async reportProgress(data) {
|
||||
const timestamp = Date.now() - this.startTime;
|
||||
|
||||
const entry = {
|
||||
timestamp,
|
||||
...data
|
||||
};
|
||||
|
||||
this.progressHistory.push(entry);
|
||||
|
||||
if (this.enableDebug) {
|
||||
const percentage = data.total
|
||||
? Math.round((data.progress / data.total) * 100)
|
||||
: 0;
|
||||
console.log(
|
||||
chalk.blue(`[${timestamp}ms]`),
|
||||
chalk.green(`${percentage}%`),
|
||||
chalk.yellow(data.message)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
getProgressHistory() {
|
||||
return this.progressHistory;
|
||||
}
|
||||
|
||||
printSummary() {
|
||||
console.log(chalk.green('\n=== Progress Summary ==='));
|
||||
console.log(`Total progress reports: ${this.progressHistory.length}`);
|
||||
console.log(
|
||||
`Duration: ${this.progressHistory[this.progressHistory.length - 1]?.timestamp || 0}ms`
|
||||
);
|
||||
|
||||
this.progressHistory.forEach((entry, index) => {
|
||||
const percentage = entry.total
|
||||
? Math.round((entry.progress / entry.total) * 100)
|
||||
: 0;
|
||||
console.log(
|
||||
`${index + 1}. [${entry.timestamp}ms] ${percentage}% - ${entry.message}`
|
||||
);
|
||||
});
|
||||
|
||||
// Check for expected message formats
|
||||
const hasInitialMessage = this.progressHistory.some(
|
||||
(entry) =>
|
||||
entry.message.includes('Starting PRD analysis') &&
|
||||
entry.message.includes('Input:') &&
|
||||
entry.message.includes('tokens')
|
||||
);
|
||||
// Make regex more flexible to handle potential whitespace variations
|
||||
const hasTaskMessages = this.progressHistory.some((entry) =>
|
||||
/^[🔴🟠🟢⚪]{3} Task \d+\/\d+ - .+ \| ~Output: \d+ tokens/u.test(
|
||||
entry.message.trim()
|
||||
)
|
||||
);
|
||||
|
||||
const hasCompletionMessage = this.progressHistory.some(
|
||||
(entry) =>
|
||||
entry.message.includes('✅ Task Generation Completed') &&
|
||||
entry.message.includes('Tokens (I/O):')
|
||||
);
|
||||
|
||||
console.log(chalk.cyan('\n=== Message Format Validation ==='));
|
||||
console.log(
|
||||
`✅ Initial message format: ${hasInitialMessage ? 'PASS' : 'FAIL'}`
|
||||
);
|
||||
console.log(`✅ Task message format: ${hasTaskMessages ? 'PASS' : 'FAIL'}`);
|
||||
console.log(
|
||||
`✅ Completion message format: ${hasCompletionMessage ? 'PASS' : 'FAIL'}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock MCP Logger for testing
|
||||
*/
|
||||
class MockMCPLogger {
|
||||
constructor(enableDebug = true) {
|
||||
this.enableDebug = enableDebug;
|
||||
this.logs = [];
|
||||
}
|
||||
|
||||
_log(level, ...args) {
|
||||
const entry = {
|
||||
level,
|
||||
timestamp: Date.now(),
|
||||
message: args.join(' ')
|
||||
};
|
||||
this.logs.push(entry);
|
||||
|
||||
if (this.enableDebug) {
|
||||
const color =
|
||||
{
|
||||
info: chalk.blue,
|
||||
warn: chalk.yellow,
|
||||
error: chalk.red,
|
||||
debug: chalk.gray,
|
||||
success: chalk.green
|
||||
}[level] || chalk.white;
|
||||
|
||||
console.log(color(`[${level.toUpperCase()}]`), ...args);
|
||||
}
|
||||
}
|
||||
|
||||
info(...args) {
|
||||
this._log('info', ...args);
|
||||
}
|
||||
warn(...args) {
|
||||
this._log('warn', ...args);
|
||||
}
|
||||
error(...args) {
|
||||
this._log('error', ...args);
|
||||
}
|
||||
debug(...args) {
|
||||
this._log('debug', ...args);
|
||||
}
|
||||
success(...args) {
|
||||
this._log('success', ...args);
|
||||
}
|
||||
|
||||
getLogs() {
|
||||
return this.logs;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the sample PRD file
|
||||
*/
|
||||
function getSamplePRDPath() {
|
||||
return path.resolve(PROJECT_ROOT, 'tests', 'fixtures', 'sample-prd.txt');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a basic test config file
|
||||
*/
|
||||
function createTestConfig() {
|
||||
const testConfig = {
|
||||
models: {
|
||||
main: {
|
||||
provider: 'anthropic',
|
||||
modelId: 'claude-3-5-sonnet',
|
||||
maxTokens: 64000,
|
||||
temperature: 0.2
|
||||
},
|
||||
research: {
|
||||
provider: 'perplexity',
|
||||
modelId: 'sonar-pro',
|
||||
maxTokens: 8700,
|
||||
temperature: 0.1
|
||||
},
|
||||
fallback: {
|
||||
provider: 'anthropic',
|
||||
modelId: 'claude-3-5-sonnet',
|
||||
maxTokens: 64000,
|
||||
temperature: 0.2
|
||||
}
|
||||
},
|
||||
global: {
|
||||
logLevel: 'info',
|
||||
debug: false,
|
||||
defaultSubtasks: 5,
|
||||
defaultPriority: 'medium',
|
||||
projectName: 'Task Master Test',
|
||||
ollamaBaseURL: 'http://localhost:11434/api',
|
||||
bedrockBaseURL: 'https://bedrock.us-east-1.amazonaws.com'
|
||||
}
|
||||
};
|
||||
|
||||
const taskmasterDir = path.join(__dirname, '.taskmaster');
|
||||
const configPath = path.join(taskmasterDir, 'config.json');
|
||||
|
||||
// Create .taskmaster directory if it doesn't exist
|
||||
if (!fs.existsSync(taskmasterDir)) {
|
||||
fs.mkdirSync(taskmasterDir, { recursive: true });
|
||||
}
|
||||
|
||||
fs.writeFileSync(configPath, JSON.stringify(testConfig, null, 2));
|
||||
return configPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup test files and configuration
|
||||
*/
|
||||
function setupTestFiles(testName) {
|
||||
const testPRDPath = getSamplePRDPath();
|
||||
const testTasksPath = path.join(__dirname, `test-${testName}-tasks.json`);
|
||||
const configPath = createTestConfig();
|
||||
|
||||
// Clean up existing files
|
||||
if (fs.existsSync(testTasksPath)) {
|
||||
fs.unlinkSync(testTasksPath);
|
||||
}
|
||||
|
||||
return { testPRDPath, testTasksPath, configPath };
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up test files
|
||||
*/
|
||||
function cleanupTestFiles(testTasksPath, configPath) {
|
||||
if (fs.existsSync(testTasksPath)) fs.unlinkSync(testTasksPath);
|
||||
if (fs.existsSync(configPath)) fs.unlinkSync(configPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run parsePRD with configurable options
|
||||
*/
|
||||
async function runParsePRD(testPRDPath, testTasksPath, numTasks, options = {}) {
|
||||
const startTime = Date.now();
|
||||
|
||||
const result = await parsePRD(testPRDPath, testTasksPath, numTasks, {
|
||||
force: true,
|
||||
append: false,
|
||||
research: false,
|
||||
projectRoot: PROJECT_ROOT,
|
||||
...options
|
||||
});
|
||||
|
||||
const endTime = Date.now();
|
||||
const duration = endTime - startTime;
|
||||
|
||||
return { result, duration };
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify task file existence and structure
|
||||
*/
|
||||
function verifyTaskResults(testTasksPath) {
|
||||
if (fs.existsSync(testTasksPath)) {
|
||||
const tasksData = JSON.parse(fs.readFileSync(testTasksPath, 'utf8'));
|
||||
console.log(
|
||||
chalk.green(
|
||||
`\n✅ Tasks file created with ${tasksData.tasks.length} tasks`
|
||||
)
|
||||
);
|
||||
|
||||
// Verify task structure
|
||||
const firstTask = tasksData.tasks[0];
|
||||
if (firstTask && firstTask.id && firstTask.title && firstTask.description) {
|
||||
console.log(chalk.green('✅ Task structure is valid'));
|
||||
return true;
|
||||
} else {
|
||||
console.log(chalk.red('❌ Task structure is invalid'));
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
console.log(chalk.red('❌ Tasks file was not created'));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Print MCP-specific logs and validation
|
||||
*/
|
||||
function printMCPResults(mcpLogger, progressReporter) {
|
||||
// Print progress summary
|
||||
progressReporter.printSummary();
|
||||
|
||||
// Print MCP logs
|
||||
console.log(chalk.cyan('\n=== MCP Logs ==='));
|
||||
const logs = mcpLogger.getLogs();
|
||||
logs.forEach((log, index) => {
|
||||
const color =
|
||||
{
|
||||
info: chalk.blue,
|
||||
warn: chalk.yellow,
|
||||
error: chalk.red,
|
||||
debug: chalk.gray,
|
||||
success: chalk.green
|
||||
}[log.level] || chalk.white;
|
||||
console.log(
|
||||
`${index + 1}. ${color(`[${log.level.toUpperCase()}]`)} ${log.message}`
|
||||
);
|
||||
});
|
||||
|
||||
// Verify MCP-specific message formats (should use emoji indicators)
|
||||
const hasEmojiIndicators = progressReporter
|
||||
.getProgressHistory()
|
||||
.some((entry) => /[🔴🟠🟢]/u.test(entry.message));
|
||||
|
||||
console.log(chalk.cyan('\n=== MCP-Specific Validation ==='));
|
||||
console.log(
|
||||
`✅ Emoji priority indicators: ${hasEmojiIndicators ? 'PASS' : 'FAIL'}`
|
||||
);
|
||||
|
||||
return { hasEmojiIndicators, logs };
|
||||
}
|
||||
|
||||
/**
|
||||
* Test MCP streaming with proper MCP context
|
||||
*/
|
||||
async function testMCPStreaming(numTasks = 10) {
|
||||
console.log(chalk.cyan('🧪 Testing MCP Streaming Functionality\n'));
|
||||
|
||||
const { testPRDPath, testTasksPath, configPath } = setupTestFiles('mcp');
|
||||
const progressReporter = new MockProgressReporter(true);
|
||||
const mcpLogger = new MockMCPLogger(true); // Enable debug for MCP context
|
||||
|
||||
try {
|
||||
console.log(chalk.yellow('Starting MCP streaming test...'));
|
||||
|
||||
const { result, duration } = await runParsePRD(
|
||||
testPRDPath,
|
||||
testTasksPath,
|
||||
numTasks,
|
||||
{
|
||||
reportProgress: progressReporter.reportProgress.bind(progressReporter),
|
||||
mcpLog: mcpLogger // Add MCP context - this is the key difference
|
||||
}
|
||||
);
|
||||
|
||||
console.log(
|
||||
chalk.green(`\n✅ MCP streaming test completed in ${duration}ms`)
|
||||
);
|
||||
|
||||
const { hasEmojiIndicators, logs } = printMCPResults(
|
||||
mcpLogger,
|
||||
progressReporter
|
||||
);
|
||||
const isValidStructure = verifyTaskResults(testTasksPath);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
duration,
|
||||
progressHistory: progressReporter.getProgressHistory(),
|
||||
mcpLogs: logs,
|
||||
hasEmojiIndicators,
|
||||
result
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`❌ MCP streaming test failed: ${error.message}`));
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
} finally {
|
||||
cleanupTestFiles(testTasksPath, configPath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test CLI streaming (no reportProgress)
|
||||
*/
|
||||
async function testCLIStreaming(numTasks = 10) {
|
||||
console.log(chalk.cyan('🧪 Testing CLI Streaming (No Progress Reporter)\n'));
|
||||
|
||||
const { testPRDPath, testTasksPath, configPath } = setupTestFiles('cli');
|
||||
|
||||
try {
|
||||
console.log(chalk.yellow('Starting CLI streaming test...'));
|
||||
|
||||
// No reportProgress provided; CLI text mode uses the default streaming reporter
|
||||
const { result, duration } = await runParsePRD(
|
||||
testPRDPath,
|
||||
testTasksPath,
|
||||
numTasks
|
||||
);
|
||||
|
||||
console.log(
|
||||
chalk.green(`\n✅ CLI streaming test completed in ${duration}ms`)
|
||||
);
|
||||
|
||||
const isValidStructure = verifyTaskResults(testTasksPath);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
duration,
|
||||
result
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`❌ CLI streaming test failed: ${error.message}`));
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
} finally {
|
||||
cleanupTestFiles(testTasksPath, configPath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test non-streaming functionality
|
||||
*/
|
||||
async function testNonStreaming(numTasks = 10) {
|
||||
console.log(chalk.cyan('🧪 Testing Non-Streaming Functionality\n'));
|
||||
|
||||
const { testPRDPath, testTasksPath, configPath } =
|
||||
setupTestFiles('non-streaming');
|
||||
|
||||
try {
|
||||
console.log(chalk.yellow('Starting non-streaming test...'));
|
||||
|
||||
// Force non-streaming by not providing reportProgress
|
||||
const { result, duration } = await runParsePRD(
|
||||
testPRDPath,
|
||||
testTasksPath,
|
||||
numTasks
|
||||
);
|
||||
|
||||
console.log(
|
||||
chalk.green(`\n✅ Non-streaming test completed in ${duration}ms`)
|
||||
);
|
||||
|
||||
const isValidStructure = verifyTaskResults(testTasksPath);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
duration,
|
||||
result
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`❌ Non-streaming test failed: ${error.message}`));
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
} finally {
|
||||
cleanupTestFiles(testTasksPath, configPath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare results between streaming and non-streaming
|
||||
*/
|
||||
function compareResults(streamingResult, nonStreamingResult) {
|
||||
console.log(chalk.cyan('\n=== Results Comparison ==='));
|
||||
|
||||
if (!streamingResult.success || !nonStreamingResult.success) {
|
||||
console.log(chalk.red('❌ Cannot compare - one or both tests failed'));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Streaming duration: ${streamingResult.duration}ms`);
|
||||
console.log(`Non-streaming duration: ${nonStreamingResult.duration}ms`);
|
||||
|
||||
const durationDiff = Math.abs(
|
||||
streamingResult.duration - nonStreamingResult.duration
|
||||
);
|
||||
const durationDiffPercent = Math.round(
|
||||
(durationDiff /
|
||||
Math.max(streamingResult.duration, nonStreamingResult.duration)) *
|
||||
100
|
||||
);
|
||||
|
||||
console.log(
|
||||
`Duration difference: ${durationDiff}ms (${durationDiffPercent}%)`
|
||||
);
|
||||
|
||||
if (streamingResult.progressHistory) {
|
||||
console.log(
|
||||
`Streaming progress reports: ${streamingResult.progressHistory.length}`
|
||||
);
|
||||
}
|
||||
|
||||
console.log(chalk.green('✅ Both methods completed successfully'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Main test runner
|
||||
*/
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const testType = args[0] || 'streaming';
|
||||
const numTasks = parseInt(args[1]) || 8;
|
||||
|
||||
console.log(chalk.bold.cyan('🚀 Task Master PRD Streaming Tests\n'));
|
||||
console.log(chalk.blue(`Test type: ${testType}`));
|
||||
console.log(chalk.blue(`Number of tasks: ${numTasks}\n`));
|
||||
|
||||
try {
|
||||
switch (testType.toLowerCase()) {
|
||||
case 'mcp':
|
||||
case 'mcp-streaming':
|
||||
await testMCPStreaming(numTasks);
|
||||
break;
|
||||
|
||||
case 'cli':
|
||||
case 'cli-streaming':
|
||||
await testCLIStreaming(numTasks);
|
||||
break;
|
||||
|
||||
case 'non-streaming':
|
||||
case 'non':
|
||||
await testNonStreaming(numTasks);
|
||||
break;
|
||||
|
||||
case 'both': {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'Running both MCP streaming and non-streaming tests...\n'
|
||||
)
|
||||
);
|
||||
const mcpStreamingResult = await testMCPStreaming(numTasks);
|
||||
console.log('\n' + '='.repeat(60) + '\n');
|
||||
const nonStreamingResult = await testNonStreaming(numTasks);
|
||||
compareResults(mcpStreamingResult, nonStreamingResult);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'all': {
|
||||
console.log(chalk.yellow('Running all test types...\n'));
|
||||
const mcpResult = await testMCPStreaming(numTasks);
|
||||
console.log('\n' + '='.repeat(60) + '\n');
|
||||
const cliResult = await testCLIStreaming(numTasks);
|
||||
console.log('\n' + '='.repeat(60) + '\n');
|
||||
const nonStreamResult = await testNonStreaming(numTasks);
|
||||
|
||||
console.log(chalk.cyan('\n=== All Tests Summary ==='));
|
||||
console.log(
|
||||
`MCP Streaming: ${mcpResult.success ? '✅ PASS' : '❌ FAIL'} ${mcpResult.hasEmojiIndicators ? '(✅ Emojis)' : '(❌ No Emojis)'}`
|
||||
);
|
||||
console.log(
|
||||
`CLI Streaming: ${cliResult.success ? '✅ PASS' : '❌ FAIL'}`
|
||||
);
|
||||
console.log(
|
||||
`Non-streaming: ${nonStreamResult.success ? '✅ PASS' : '❌ FAIL'}`
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
console.log(chalk.red(`Unknown test type: ${testType}`));
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'Available options: mcp-streaming, cli-streaming, non-streaming, both, all'
|
||||
)
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(chalk.green('\n🎉 Tests completed successfully!'));
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`\n❌ Test failed: ${error.message}`));
|
||||
console.error(chalk.red(error.stack));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
main();
|
||||
}
|
||||
@@ -391,7 +391,7 @@ describe('Unified AI Services', () => {
|
||||
expect.stringContaining('Service call failed for role main')
|
||||
);
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'info',
|
||||
'debug',
|
||||
expect.stringContaining('New AI service call with role: fallback')
|
||||
);
|
||||
});
|
||||
@@ -435,7 +435,7 @@ describe('Unified AI Services', () => {
|
||||
expect.stringContaining('Service call failed for role fallback')
|
||||
);
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'info',
|
||||
'debug',
|
||||
expect.stringContaining('New AI service call with role: research')
|
||||
);
|
||||
});
|
||||
|
||||
@@ -145,6 +145,7 @@ const DEFAULT_CONFIG = {
|
||||
projectName: 'Task Master',
|
||||
ollamaBaseURL: 'http://localhost:11434/api',
|
||||
bedrockBaseURL: 'https://bedrock.us-east-1.amazonaws.com',
|
||||
enableCodebaseAnalysis: true,
|
||||
responseLanguage: 'English'
|
||||
},
|
||||
claudeCode: {}
|
||||
|
||||
135
tests/unit/mcp/tools/move-task-cross-tag-options.test.js
Normal file
135
tests/unit/mcp/tools/move-task-cross-tag-options.test.js
Normal file
@@ -0,0 +1,135 @@
|
||||
import { jest } from '@jest/globals';
|
||||
|
||||
// Mocks
|
||||
const mockFindTasksPath = jest
|
||||
.fn()
|
||||
.mockReturnValue('/test/path/.taskmaster/tasks/tasks.json');
|
||||
jest.unstable_mockModule(
|
||||
'../../../../mcp-server/src/core/utils/path-utils.js',
|
||||
() => ({
|
||||
findTasksPath: mockFindTasksPath
|
||||
})
|
||||
);
|
||||
|
||||
const mockEnableSilentMode = jest.fn();
|
||||
const mockDisableSilentMode = jest.fn();
|
||||
jest.unstable_mockModule('../../../../scripts/modules/utils.js', () => ({
|
||||
enableSilentMode: mockEnableSilentMode,
|
||||
disableSilentMode: mockDisableSilentMode
|
||||
}));
|
||||
|
||||
// Spyable mock for moveTasksBetweenTags
|
||||
const mockMoveTasksBetweenTags = jest.fn();
|
||||
jest.unstable_mockModule(
|
||||
'../../../../scripts/modules/task-manager/move-task.js',
|
||||
() => ({
|
||||
moveTasksBetweenTags: mockMoveTasksBetweenTags
|
||||
})
|
||||
);
|
||||
|
||||
// Import after mocks
|
||||
const { moveTaskCrossTagDirect } = await import(
|
||||
'../../../../mcp-server/src/core/direct-functions/move-task-cross-tag.js'
|
||||
);
|
||||
|
||||
describe('MCP Cross-Tag Move Direct Function - options & suggestions', () => {
|
||||
const mockLog = { info: jest.fn(), warn: jest.fn(), error: jest.fn() };
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('passes only withDependencies/ignoreDependencies (no force) to core', async () => {
|
||||
// Arrange: make core throw tag validation after call to capture params
|
||||
mockMoveTasksBetweenTags.mockImplementation(() => {
|
||||
const err = new Error('Source tag "invalid" not found or invalid');
|
||||
err.code = 'INVALID_SOURCE_TAG';
|
||||
throw err;
|
||||
});
|
||||
|
||||
// Act
|
||||
await moveTaskCrossTagDirect(
|
||||
{
|
||||
sourceIds: '1,2',
|
||||
sourceTag: 'backlog',
|
||||
targetTag: 'in-progress',
|
||||
withDependencies: true,
|
||||
projectRoot: '/test'
|
||||
},
|
||||
mockLog
|
||||
);
|
||||
|
||||
// Assert options argument (5th param)
|
||||
expect(mockMoveTasksBetweenTags).toHaveBeenCalled();
|
||||
const args = mockMoveTasksBetweenTags.mock.calls[0];
|
||||
const moveOptions = args[4];
|
||||
expect(moveOptions).toEqual({
|
||||
withDependencies: true,
|
||||
ignoreDependencies: false
|
||||
});
|
||||
expect('force' in moveOptions).toBe(false);
|
||||
});
|
||||
|
||||
it('returns conflict suggestions on cross-tag dependency conflicts', async () => {
|
||||
// Arrange: core throws cross-tag dependency conflicts
|
||||
mockMoveTasksBetweenTags.mockImplementation(() => {
|
||||
const err = new Error(
|
||||
'Cannot move tasks: 2 cross-tag dependency conflicts found'
|
||||
);
|
||||
err.code = 'CROSS_TAG_DEPENDENCY_CONFLICTS';
|
||||
throw err;
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = await moveTaskCrossTagDirect(
|
||||
{
|
||||
sourceIds: '1',
|
||||
sourceTag: 'backlog',
|
||||
targetTag: 'in-progress',
|
||||
projectRoot: '/test'
|
||||
},
|
||||
mockLog
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error.code).toBe('CROSS_TAG_DEPENDENCY_CONFLICT');
|
||||
expect(Array.isArray(result.error.suggestions)).toBe(true);
|
||||
// Key suggestions
|
||||
const s = result.error.suggestions.join(' ');
|
||||
expect(s).toContain('--with-dependencies');
|
||||
expect(s).toContain('--ignore-dependencies');
|
||||
expect(s).toContain('validate-dependencies');
|
||||
expect(s).toContain('Move dependencies first');
|
||||
});
|
||||
|
||||
it('returns ID collision suggestions when target tag already has the ID', async () => {
|
||||
// Arrange: core throws TASK_ALREADY_EXISTS structured error
|
||||
mockMoveTasksBetweenTags.mockImplementation(() => {
|
||||
const err = new Error(
|
||||
'Task 1 already exists in target tag "in-progress"'
|
||||
);
|
||||
err.code = 'TASK_ALREADY_EXISTS';
|
||||
throw err;
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = await moveTaskCrossTagDirect(
|
||||
{
|
||||
sourceIds: '1',
|
||||
sourceTag: 'backlog',
|
||||
targetTag: 'in-progress',
|
||||
projectRoot: '/test'
|
||||
},
|
||||
mockLog
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error.code).toBe('TASK_ALREADY_EXISTS');
|
||||
const joined = (result.error.suggestions || []).join(' ');
|
||||
expect(joined).toContain('different target tag');
|
||||
expect(joined).toContain('different set of IDs');
|
||||
expect(joined).toContain('within-tag');
|
||||
});
|
||||
});
|
||||
@@ -1,68 +1,471 @@
|
||||
// In tests/unit/parse-prd.test.js
|
||||
// Testing that parse-prd.js handles both .txt and .md files the same way
|
||||
// Testing parse-prd.js file extension compatibility with real files
|
||||
|
||||
import { jest } from '@jest/globals';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import os from 'os';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// Mock the AI services to avoid real API calls
|
||||
jest.unstable_mockModule(
|
||||
'../../scripts/modules/ai-services-unified.js',
|
||||
() => ({
|
||||
streamTextService: jest.fn(),
|
||||
generateObjectService: jest.fn(),
|
||||
streamObjectService: jest.fn().mockImplementation(async () => {
|
||||
return {
|
||||
get partialObjectStream() {
|
||||
return (async function* () {
|
||||
yield { tasks: [] };
|
||||
yield { tasks: [{ id: 1, title: 'Test Task', priority: 'high' }] };
|
||||
})();
|
||||
},
|
||||
object: Promise.resolve({
|
||||
tasks: [{ id: 1, title: 'Test Task', priority: 'high' }]
|
||||
})
|
||||
};
|
||||
})
|
||||
})
|
||||
);
|
||||
|
||||
// Mock all config-manager exports comprehensively
|
||||
jest.unstable_mockModule('../../scripts/modules/config-manager.js', () => ({
|
||||
getDebugFlag: jest.fn(() => false),
|
||||
getDefaultPriority: jest.fn(() => 'medium'),
|
||||
getMainModelId: jest.fn(() => 'test-model'),
|
||||
getResearchModelId: jest.fn(() => 'test-research-model'),
|
||||
getParametersForRole: jest.fn(() => ({ maxTokens: 1000, temperature: 0.7 })),
|
||||
getMainProvider: jest.fn(() => 'anthropic'),
|
||||
getResearchProvider: jest.fn(() => 'perplexity'),
|
||||
getFallbackProvider: jest.fn(() => 'anthropic'),
|
||||
getResponseLanguage: jest.fn(() => 'English'),
|
||||
getDefaultNumTasks: jest.fn(() => 10),
|
||||
getDefaultSubtasks: jest.fn(() => 5),
|
||||
getLogLevel: jest.fn(() => 'info'),
|
||||
getConfig: jest.fn(() => ({})),
|
||||
getAllProviders: jest.fn(() => ['anthropic', 'perplexity']),
|
||||
MODEL_MAP: {},
|
||||
VALID_PROVIDERS: ['anthropic', 'perplexity'],
|
||||
validateProvider: jest.fn(() => true),
|
||||
validateProviderModelCombination: jest.fn(() => true),
|
||||
isApiKeySet: jest.fn(() => true),
|
||||
hasCodebaseAnalysis: jest.fn(() => false)
|
||||
}));
|
||||
|
||||
// Mock utils comprehensively to prevent CLI behavior
|
||||
jest.unstable_mockModule('../../scripts/modules/utils.js', () => ({
|
||||
log: jest.fn(),
|
||||
writeJSON: jest.fn(),
|
||||
enableSilentMode: jest.fn(),
|
||||
disableSilentMode: jest.fn(),
|
||||
isSilentMode: jest.fn(() => false),
|
||||
getCurrentTag: jest.fn(() => 'master'),
|
||||
ensureTagMetadata: jest.fn(),
|
||||
readJSON: jest.fn(() => ({ master: { tasks: [] } })),
|
||||
findProjectRoot: jest.fn(() => '/tmp/test'),
|
||||
resolveEnvVariable: jest.fn(() => 'mock-key'),
|
||||
findTaskById: jest.fn(() => null),
|
||||
findTaskByPattern: jest.fn(() => []),
|
||||
validateTaskId: jest.fn(() => true),
|
||||
createTask: jest.fn(() => ({ id: 1, title: 'Mock Task' })),
|
||||
sortByDependencies: jest.fn((tasks) => tasks),
|
||||
isEmpty: jest.fn(() => false),
|
||||
truncate: jest.fn((text) => text),
|
||||
slugify: jest.fn((text) => text.toLowerCase()),
|
||||
getTagFromPath: jest.fn(() => 'master'),
|
||||
isValidTag: jest.fn(() => true),
|
||||
migrateToTaggedFormat: jest.fn(() => ({ master: { tasks: [] } })),
|
||||
performCompleteTagMigration: jest.fn(),
|
||||
resolveCurrentTag: jest.fn(() => 'master'),
|
||||
getDefaultTag: jest.fn(() => 'master'),
|
||||
performMigrationIfNeeded: jest.fn()
|
||||
}));
|
||||
|
||||
// Mock prompt manager
|
||||
jest.unstable_mockModule('../../scripts/modules/prompt-manager.js', () => ({
|
||||
getPromptManager: jest.fn(() => ({
|
||||
loadPrompt: jest.fn(() => ({
|
||||
systemPrompt: 'Test system prompt',
|
||||
userPrompt: 'Test user prompt'
|
||||
}))
|
||||
}))
|
||||
}));
|
||||
|
||||
// Mock progress/UI components to prevent real CLI UI
|
||||
jest.unstable_mockModule('../../src/progress/parse-prd-tracker.js', () => ({
|
||||
createParsePrdTracker: jest.fn(() => ({
|
||||
start: jest.fn(),
|
||||
stop: jest.fn(),
|
||||
cleanup: jest.fn(),
|
||||
addTaskLine: jest.fn(),
|
||||
updateTokens: jest.fn(),
|
||||
complete: jest.fn(),
|
||||
getSummary: jest.fn().mockReturnValue({
|
||||
taskPriorities: { high: 0, medium: 0, low: 0 },
|
||||
elapsedTime: 0,
|
||||
actionVerb: 'generated'
|
||||
})
|
||||
}))
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule('../../src/ui/parse-prd.js', () => ({
|
||||
displayParsePrdStart: jest.fn(),
|
||||
displayParsePrdSummary: jest.fn()
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule('../../scripts/modules/ui.js', () => ({
|
||||
displayAiUsageSummary: jest.fn()
|
||||
}));
|
||||
|
||||
// Mock task generation to prevent file operations
|
||||
jest.unstable_mockModule(
|
||||
'../../scripts/modules/task-manager/generate-task-files.js',
|
||||
() => ({
|
||||
default: jest.fn()
|
||||
})
|
||||
);
|
||||
|
||||
// Mock stream parser
|
||||
jest.unstable_mockModule('../../src/utils/stream-parser.js', () => {
|
||||
// Define mock StreamingError class
|
||||
class StreamingError extends Error {
|
||||
constructor(message, code) {
|
||||
super(message);
|
||||
this.name = 'StreamingError';
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
// Define mock error codes
|
||||
const STREAMING_ERROR_CODES = {
|
||||
NOT_ASYNC_ITERABLE: 'STREAMING_NOT_SUPPORTED',
|
||||
STREAM_PROCESSING_FAILED: 'STREAM_PROCESSING_FAILED',
|
||||
STREAM_NOT_ITERABLE: 'STREAM_NOT_ITERABLE'
|
||||
};
|
||||
|
||||
return {
|
||||
parseStream: jest.fn(),
|
||||
StreamingError,
|
||||
STREAMING_ERROR_CODES
|
||||
};
|
||||
});
|
||||
|
||||
// Mock other potential UI elements
|
||||
jest.unstable_mockModule('ora', () => ({
|
||||
default: jest.fn(() => ({
|
||||
start: jest.fn(),
|
||||
stop: jest.fn(),
|
||||
succeed: jest.fn(),
|
||||
fail: jest.fn()
|
||||
}))
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule('chalk', () => ({
|
||||
default: {
|
||||
red: jest.fn((text) => text),
|
||||
green: jest.fn((text) => text),
|
||||
blue: jest.fn((text) => text),
|
||||
yellow: jest.fn((text) => text),
|
||||
cyan: jest.fn((text) => text),
|
||||
white: {
|
||||
bold: jest.fn((text) => text)
|
||||
}
|
||||
},
|
||||
red: jest.fn((text) => text),
|
||||
green: jest.fn((text) => text),
|
||||
blue: jest.fn((text) => text),
|
||||
yellow: jest.fn((text) => text),
|
||||
cyan: jest.fn((text) => text),
|
||||
white: {
|
||||
bold: jest.fn((text) => text)
|
||||
}
|
||||
}));
|
||||
|
||||
// Mock boxen
|
||||
jest.unstable_mockModule('boxen', () => ({
|
||||
default: jest.fn((content) => content)
|
||||
}));
|
||||
|
||||
// Mock constants
|
||||
jest.unstable_mockModule('../../src/constants/task-priority.js', () => ({
|
||||
DEFAULT_TASK_PRIORITY: 'medium',
|
||||
TASK_PRIORITY_OPTIONS: ['low', 'medium', 'high']
|
||||
}));
|
||||
|
||||
// Mock UI indicators
|
||||
jest.unstable_mockModule('../../src/ui/indicators.js', () => ({
|
||||
getPriorityIndicators: jest.fn(() => ({
|
||||
high: '🔴',
|
||||
medium: '🟡',
|
||||
low: '🟢'
|
||||
}))
|
||||
}));
|
||||
|
||||
// Import modules after mocking
|
||||
const { generateObjectService } = await import(
|
||||
'../../scripts/modules/ai-services-unified.js'
|
||||
);
|
||||
const parsePRD = (
|
||||
await import('../../scripts/modules/task-manager/parse-prd/parse-prd.js')
|
||||
).default;
|
||||
|
||||
describe('parse-prd file extension compatibility', () => {
|
||||
// Test directly that the parse-prd functionality works with different extensions
|
||||
// by examining the parameter handling in mcp-server/src/tools/parse-prd.js
|
||||
let tempDir;
|
||||
let testFiles;
|
||||
|
||||
test('Parameter description mentions support for .md files', () => {
|
||||
// The parameter description for 'input' in parse-prd.js includes .md files
|
||||
const description =
|
||||
'Absolute path to the PRD document file (.txt, .md, etc.)';
|
||||
|
||||
// Verify the description explicitly mentions .md files
|
||||
expect(description).toContain('.md');
|
||||
});
|
||||
|
||||
test('File extension validation is not restricted to .txt files', () => {
|
||||
// Check for absence of extension validation
|
||||
const fileValidator = (filePath) => {
|
||||
// Return a boolean value to ensure the test passes
|
||||
if (!filePath || filePath.length === 0) {
|
||||
return false;
|
||||
const mockTasksResponse = {
|
||||
tasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Test Task 1',
|
||||
description: 'First test task',
|
||||
status: 'pending',
|
||||
dependencies: [],
|
||||
priority: 'high',
|
||||
details: 'Implementation details for task 1',
|
||||
testStrategy: 'Unit tests for task 1'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Test Task 2',
|
||||
description: 'Second test task',
|
||||
status: 'pending',
|
||||
dependencies: [1],
|
||||
priority: 'medium',
|
||||
details: 'Implementation details for task 2',
|
||||
testStrategy: 'Integration tests for task 2'
|
||||
}
|
||||
return true;
|
||||
],
|
||||
metadata: {
|
||||
projectName: 'Test Project',
|
||||
totalTasks: 2,
|
||||
sourceFile: 'test-prd',
|
||||
generatedAt: new Date().toISOString()
|
||||
}
|
||||
};
|
||||
|
||||
const samplePRDContent = `# Test Project PRD
|
||||
|
||||
## Overview
|
||||
Build a simple task management application.
|
||||
|
||||
## Features
|
||||
1. Create and manage tasks
|
||||
2. Set task priorities
|
||||
3. Track task dependencies
|
||||
|
||||
## Technical Requirements
|
||||
- React frontend
|
||||
- Node.js backend
|
||||
- PostgreSQL database
|
||||
|
||||
## Success Criteria
|
||||
- Users can create tasks successfully
|
||||
- Task dependencies work correctly`;
|
||||
|
||||
beforeAll(() => {
|
||||
// Create temporary directory for test files
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'parse-prd-test-'));
|
||||
|
||||
// Create test files with different extensions
|
||||
testFiles = {
|
||||
txt: path.join(tempDir, 'test-prd.txt'),
|
||||
md: path.join(tempDir, 'test-prd.md'),
|
||||
rst: path.join(tempDir, 'test-prd.rst'),
|
||||
noExt: path.join(tempDir, 'test-prd')
|
||||
};
|
||||
|
||||
// Test with different extensions
|
||||
expect(fileValidator('/path/to/prd.txt')).toBe(true);
|
||||
expect(fileValidator('/path/to/prd.md')).toBe(true);
|
||||
// Write the same content to all test files
|
||||
Object.values(testFiles).forEach((filePath) => {
|
||||
fs.writeFileSync(filePath, samplePRDContent);
|
||||
});
|
||||
|
||||
// Invalid cases should still fail regardless of extension
|
||||
expect(fileValidator('')).toBe(false);
|
||||
// Mock process.exit to prevent actual exit
|
||||
jest.spyOn(process, 'exit').mockImplementation(() => undefined);
|
||||
|
||||
// Mock console methods to prevent output
|
||||
jest.spyOn(console, 'log').mockImplementation(() => {});
|
||||
jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
test('Implementation handles all file types the same way', () => {
|
||||
// This test confirms that the implementation treats all file types equally
|
||||
// by simulating the core functionality
|
||||
afterAll(() => {
|
||||
// Clean up temporary directory
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
|
||||
const mockImplementation = (filePath) => {
|
||||
// The parse-prd.js implementation only checks file existence,
|
||||
// not the file extension, which is what we want to verify
|
||||
// Restore mocks
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
if (!filePath) {
|
||||
return { success: false, error: { code: 'MISSING_INPUT_FILE' } };
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Mock successful AI response
|
||||
generateObjectService.mockResolvedValue({
|
||||
mainResult: { object: mockTasksResponse },
|
||||
telemetryData: {
|
||||
timestamp: new Date().toISOString(),
|
||||
userId: 'test-user',
|
||||
commandName: 'parse-prd',
|
||||
modelUsed: 'test-model',
|
||||
providerName: 'test-provider',
|
||||
inputTokens: 100,
|
||||
outputTokens: 200,
|
||||
totalTokens: 300,
|
||||
totalCost: 0.01,
|
||||
currency: 'USD'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// In the real implementation, this would check if the file exists
|
||||
// But for our test, we're verifying that the same logic applies
|
||||
// regardless of file extension
|
||||
test('should accept and parse .txt files', async () => {
|
||||
const outputPath = path.join(tempDir, 'tasks-txt.json');
|
||||
|
||||
// No special handling for different extensions
|
||||
return { success: true };
|
||||
};
|
||||
const result = await parsePRD(testFiles.txt, outputPath, 2, {
|
||||
force: true,
|
||||
mcpLog: {
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
success: jest.fn()
|
||||
},
|
||||
projectRoot: tempDir
|
||||
});
|
||||
|
||||
// Verify same behavior for different extensions
|
||||
const txtResult = mockImplementation('/path/to/prd.txt');
|
||||
const mdResult = mockImplementation('/path/to/prd.md');
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.tasksPath).toBe(outputPath);
|
||||
expect(fs.existsSync(outputPath)).toBe(true);
|
||||
|
||||
// Both should succeed since there's no extension-specific logic
|
||||
expect(txtResult.success).toBe(true);
|
||||
expect(mdResult.success).toBe(true);
|
||||
// Verify the content was parsed correctly
|
||||
const tasksData = JSON.parse(fs.readFileSync(outputPath, 'utf8'));
|
||||
expect(tasksData.master.tasks).toHaveLength(2);
|
||||
expect(tasksData.master.tasks[0].title).toBe('Test Task 1');
|
||||
});
|
||||
|
||||
// Both should have the same structure
|
||||
expect(Object.keys(txtResult)).toEqual(Object.keys(mdResult));
|
||||
test('should accept and parse .md files', async () => {
|
||||
const outputPath = path.join(tempDir, 'tasks-md.json');
|
||||
|
||||
const result = await parsePRD(testFiles.md, outputPath, 2, {
|
||||
force: true,
|
||||
mcpLog: {
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
success: jest.fn()
|
||||
},
|
||||
projectRoot: tempDir
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.tasksPath).toBe(outputPath);
|
||||
expect(fs.existsSync(outputPath)).toBe(true);
|
||||
|
||||
// Verify the content was parsed correctly
|
||||
const tasksData = JSON.parse(fs.readFileSync(outputPath, 'utf8'));
|
||||
expect(tasksData.master.tasks).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('should accept and parse files with other text extensions', async () => {
|
||||
const outputPath = path.join(tempDir, 'tasks-rst.json');
|
||||
|
||||
const result = await parsePRD(testFiles.rst, outputPath, 2, {
|
||||
force: true,
|
||||
mcpLog: {
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
success: jest.fn()
|
||||
},
|
||||
projectRoot: tempDir
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.tasksPath).toBe(outputPath);
|
||||
expect(fs.existsSync(outputPath)).toBe(true);
|
||||
});
|
||||
|
||||
test('should accept and parse files with no extension', async () => {
|
||||
const outputPath = path.join(tempDir, 'tasks-noext.json');
|
||||
|
||||
const result = await parsePRD(testFiles.noExt, outputPath, 2, {
|
||||
force: true,
|
||||
mcpLog: {
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
success: jest.fn()
|
||||
},
|
||||
projectRoot: tempDir
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.tasksPath).toBe(outputPath);
|
||||
expect(fs.existsSync(outputPath)).toBe(true);
|
||||
});
|
||||
|
||||
test('should produce identical results regardless of file extension', async () => {
|
||||
const outputs = {};
|
||||
|
||||
// Parse each file type with a unique project root to avoid ID conflicts
|
||||
for (const [ext, filePath] of Object.entries(testFiles)) {
|
||||
// Create a unique subdirectory for each test to isolate them
|
||||
const testSubDir = path.join(tempDir, `test-${ext}`);
|
||||
fs.mkdirSync(testSubDir, { recursive: true });
|
||||
|
||||
const outputPath = path.join(testSubDir, `tasks.json`);
|
||||
|
||||
await parsePRD(filePath, outputPath, 2, {
|
||||
force: true,
|
||||
mcpLog: {
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
success: jest.fn()
|
||||
},
|
||||
projectRoot: testSubDir
|
||||
});
|
||||
|
||||
const tasksData = JSON.parse(fs.readFileSync(outputPath, 'utf8'));
|
||||
outputs[ext] = tasksData;
|
||||
}
|
||||
|
||||
// Compare all outputs - they should be identical (except metadata timestamps)
|
||||
const baseOutput = outputs.txt;
|
||||
Object.values(outputs).forEach((output) => {
|
||||
expect(output.master.tasks).toEqual(baseOutput.master.tasks);
|
||||
expect(output.master.metadata.projectName).toEqual(
|
||||
baseOutput.master.metadata.projectName
|
||||
);
|
||||
expect(output.master.metadata.totalTasks).toEqual(
|
||||
baseOutput.master.metadata.totalTasks
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('should handle non-existent files gracefully', async () => {
|
||||
const nonExistentFile = path.join(tempDir, 'does-not-exist.txt');
|
||||
const outputPath = path.join(tempDir, 'tasks-error.json');
|
||||
|
||||
await expect(
|
||||
parsePRD(nonExistentFile, outputPath, 2, {
|
||||
force: true,
|
||||
mcpLog: {
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
success: jest.fn()
|
||||
},
|
||||
projectRoot: tempDir
|
||||
})
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
192
tests/unit/profiles/kilo-integration.test.js
Normal file
192
tests/unit/profiles/kilo-integration.test.js
Normal file
@@ -0,0 +1,192 @@
|
||||
import { jest } from '@jest/globals';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
|
||||
// Mock external modules
|
||||
jest.mock('child_process', () => ({
|
||||
execSync: jest.fn()
|
||||
}));
|
||||
|
||||
// Mock console methods
|
||||
jest.mock('console', () => ({
|
||||
log: jest.fn(),
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
clear: jest.fn()
|
||||
}));
|
||||
|
||||
describe('Kilo Integration', () => {
|
||||
let tempDir;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Create a temporary directory for testing
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'task-master-test-'));
|
||||
|
||||
// Spy on fs methods
|
||||
jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {});
|
||||
jest.spyOn(fs, 'readFileSync').mockImplementation((filePath) => {
|
||||
if (filePath.toString().includes('.kilocodemodes')) {
|
||||
return 'Existing kilocodemodes content';
|
||||
}
|
||||
if (filePath.toString().includes('-rules')) {
|
||||
return 'Existing mode rules content';
|
||||
}
|
||||
return '{}';
|
||||
});
|
||||
jest.spyOn(fs, 'existsSync').mockImplementation(() => false);
|
||||
jest.spyOn(fs, 'mkdirSync').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up the temporary directory
|
||||
try {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
} catch (err) {
|
||||
console.error(`Error cleaning up: ${err.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Test function that simulates the createProjectStructure behavior for Kilo files
|
||||
function mockCreateKiloStructure() {
|
||||
// Create main .kilo directory
|
||||
fs.mkdirSync(path.join(tempDir, '.kilo'), { recursive: true });
|
||||
|
||||
// Create rules directory
|
||||
fs.mkdirSync(path.join(tempDir, '.kilo', 'rules'), { recursive: true });
|
||||
|
||||
// Create mode-specific rule directories
|
||||
const kiloModes = [
|
||||
'architect',
|
||||
'ask',
|
||||
'orchestrator',
|
||||
'code',
|
||||
'debug',
|
||||
'test'
|
||||
];
|
||||
for (const mode of kiloModes) {
|
||||
fs.mkdirSync(path.join(tempDir, '.kilo', `rules-${mode}`), {
|
||||
recursive: true
|
||||
});
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, '.kilo', `rules-${mode}`, `${mode}-rules`),
|
||||
`Content for ${mode} rules`
|
||||
);
|
||||
}
|
||||
|
||||
// Create additional directories
|
||||
fs.mkdirSync(path.join(tempDir, '.kilo', 'config'), { recursive: true });
|
||||
fs.mkdirSync(path.join(tempDir, '.kilo', 'templates'), { recursive: true });
|
||||
fs.mkdirSync(path.join(tempDir, '.kilo', 'logs'), { recursive: true });
|
||||
|
||||
// Copy .kilocodemodes file
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, '.kilocodemodes'),
|
||||
'Kilocodemodes file content'
|
||||
);
|
||||
}
|
||||
|
||||
test('creates all required .kilo directories', () => {
|
||||
// Act
|
||||
mockCreateKiloStructure();
|
||||
|
||||
// Assert
|
||||
expect(fs.mkdirSync).toHaveBeenCalledWith(path.join(tempDir, '.kilo'), {
|
||||
recursive: true
|
||||
});
|
||||
expect(fs.mkdirSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.kilo', 'rules'),
|
||||
{ recursive: true }
|
||||
);
|
||||
|
||||
// Verify all mode directories are created
|
||||
expect(fs.mkdirSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.kilo', 'rules-architect'),
|
||||
{ recursive: true }
|
||||
);
|
||||
expect(fs.mkdirSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.kilo', 'rules-ask'),
|
||||
{ recursive: true }
|
||||
);
|
||||
expect(fs.mkdirSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.kilo', 'rules-orchestrator'),
|
||||
{ recursive: true }
|
||||
);
|
||||
expect(fs.mkdirSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.kilo', 'rules-code'),
|
||||
{ recursive: true }
|
||||
);
|
||||
expect(fs.mkdirSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.kilo', 'rules-debug'),
|
||||
{ recursive: true }
|
||||
);
|
||||
expect(fs.mkdirSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.kilo', 'rules-test'),
|
||||
{ recursive: true }
|
||||
);
|
||||
});
|
||||
|
||||
test('creates rule files for all modes', () => {
|
||||
// Act
|
||||
mockCreateKiloStructure();
|
||||
|
||||
// Assert - check all rule files are created
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.kilo', 'rules-architect', 'architect-rules'),
|
||||
expect.any(String)
|
||||
);
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.kilo', 'rules-ask', 'ask-rules'),
|
||||
expect.any(String)
|
||||
);
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.kilo', 'rules-orchestrator', 'orchestrator-rules'),
|
||||
expect.any(String)
|
||||
);
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.kilo', 'rules-code', 'code-rules'),
|
||||
expect.any(String)
|
||||
);
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.kilo', 'rules-debug', 'debug-rules'),
|
||||
expect.any(String)
|
||||
);
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.kilo', 'rules-test', 'test-rules'),
|
||||
expect.any(String)
|
||||
);
|
||||
});
|
||||
|
||||
test('creates .kilocodemodes file in project root', () => {
|
||||
// Act
|
||||
mockCreateKiloStructure();
|
||||
|
||||
// Assert
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.kilocodemodes'),
|
||||
expect.any(String)
|
||||
);
|
||||
});
|
||||
|
||||
test('creates additional required Kilo directories', () => {
|
||||
// Act
|
||||
mockCreateKiloStructure();
|
||||
|
||||
// Assert
|
||||
expect(fs.mkdirSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.kilo', 'config'),
|
||||
{ recursive: true }
|
||||
);
|
||||
expect(fs.mkdirSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.kilo', 'templates'),
|
||||
{ recursive: true }
|
||||
);
|
||||
expect(fs.mkdirSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.kilo', 'logs'),
|
||||
{ recursive: true }
|
||||
);
|
||||
});
|
||||
});
|
||||
216
tests/unit/profiles/rule-transformer-kilo.test.js
Normal file
216
tests/unit/profiles/rule-transformer-kilo.test.js
Normal file
@@ -0,0 +1,216 @@
|
||||
import { jest } from '@jest/globals';
|
||||
|
||||
// Mock fs module before importing anything that uses it
|
||||
jest.mock('fs', () => ({
|
||||
readFileSync: jest.fn(),
|
||||
writeFileSync: jest.fn(),
|
||||
existsSync: jest.fn(),
|
||||
mkdirSync: jest.fn()
|
||||
}));
|
||||
|
||||
// Import modules after mocking
|
||||
import fs from 'fs';
|
||||
import { convertRuleToProfileRule } from '../../../src/utils/rule-transformer.js';
|
||||
import { kiloProfile } from '../../../src/profiles/kilo.js';
|
||||
|
||||
describe('Kilo Rule Transformer', () => {
|
||||
// Set up spies on the mocked modules
|
||||
const mockReadFileSync = jest.spyOn(fs, 'readFileSync');
|
||||
const mockWriteFileSync = jest.spyOn(fs, 'writeFileSync');
|
||||
const mockExistsSync = jest.spyOn(fs, 'existsSync');
|
||||
const mockMkdirSync = jest.spyOn(fs, 'mkdirSync');
|
||||
const mockConsoleError = jest
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
// Setup default mocks
|
||||
mockReadFileSync.mockReturnValue('');
|
||||
mockWriteFileSync.mockImplementation(() => {});
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
mockMkdirSync.mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should correctly convert basic terms', () => {
|
||||
const testContent = `---
|
||||
description: Test Cursor rule for basic terms
|
||||
globs: **/*
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
This is a Cursor rule that references cursor.so and uses the word Cursor multiple times.
|
||||
Also has references to .mdc files.`;
|
||||
|
||||
// Mock file read to return our test content
|
||||
mockReadFileSync.mockReturnValue(testContent);
|
||||
|
||||
// Call the actual function
|
||||
const result = convertRuleToProfileRule(
|
||||
'source.mdc',
|
||||
'target.md',
|
||||
kiloProfile
|
||||
);
|
||||
|
||||
// Verify the function succeeded
|
||||
expect(result).toBe(true);
|
||||
|
||||
// Verify file operations were called correctly
|
||||
expect(mockReadFileSync).toHaveBeenCalledWith('source.mdc', 'utf8');
|
||||
expect(mockWriteFileSync).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Get the transformed content that was written
|
||||
const writeCall = mockWriteFileSync.mock.calls[0];
|
||||
const transformedContent = writeCall[1];
|
||||
|
||||
// Verify transformations
|
||||
expect(transformedContent).toContain('Kilo');
|
||||
expect(transformedContent).toContain('kilocode.com');
|
||||
expect(transformedContent).toContain('.md');
|
||||
expect(transformedContent).not.toContain('cursor.so');
|
||||
expect(transformedContent).not.toContain('Cursor rule');
|
||||
});
|
||||
|
||||
it('should correctly convert tool references', () => {
|
||||
const testContent = `---
|
||||
description: Test Cursor rule for tool references
|
||||
globs: **/*
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
- Use the search tool to find code
|
||||
- The edit_file tool lets you modify files
|
||||
- run_command executes terminal commands
|
||||
- use_mcp connects to external services`;
|
||||
|
||||
// Mock file read to return our test content
|
||||
mockReadFileSync.mockReturnValue(testContent);
|
||||
|
||||
// Call the actual function
|
||||
const result = convertRuleToProfileRule(
|
||||
'source.mdc',
|
||||
'target.md',
|
||||
kiloProfile
|
||||
);
|
||||
|
||||
// Verify the function succeeded
|
||||
expect(result).toBe(true);
|
||||
|
||||
// Get the transformed content that was written
|
||||
const writeCall = mockWriteFileSync.mock.calls[0];
|
||||
const transformedContent = writeCall[1];
|
||||
|
||||
// Verify transformations (Kilo uses different tool names)
|
||||
expect(transformedContent).toContain('search_files tool');
|
||||
expect(transformedContent).toContain('apply_diff tool');
|
||||
expect(transformedContent).toContain('execute_command');
|
||||
expect(transformedContent).toContain('use_mcp_tool');
|
||||
});
|
||||
|
||||
it('should correctly update file references', () => {
|
||||
const testContent = `---
|
||||
description: Test Cursor rule for file references
|
||||
globs: **/*
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
This references [dev_workflow.mdc](mdc:.cursor/rules/dev_workflow.mdc) and
|
||||
[taskmaster.mdc](mdc:.cursor/rules/taskmaster.mdc).`;
|
||||
|
||||
// Mock file read to return our test content
|
||||
mockReadFileSync.mockReturnValue(testContent);
|
||||
|
||||
// Call the actual function
|
||||
const result = convertRuleToProfileRule(
|
||||
'source.mdc',
|
||||
'target.md',
|
||||
kiloProfile
|
||||
);
|
||||
|
||||
// Verify the function succeeded
|
||||
expect(result).toBe(true);
|
||||
|
||||
// Get the transformed content that was written
|
||||
const writeCall = mockWriteFileSync.mock.calls[0];
|
||||
const transformedContent = writeCall[1];
|
||||
|
||||
// Verify transformations - no taskmaster subdirectory for Kilo
|
||||
expect(transformedContent).toContain('(.kilo/rules/dev_workflow.md)'); // File path transformation for dev_workflow - no taskmaster subdirectory for Kilo
|
||||
expect(transformedContent).toContain('(.kilo/rules/taskmaster.md)'); // File path transformation for taskmaster - no taskmaster subdirectory for Kilo
|
||||
expect(transformedContent).not.toContain('(mdc:.cursor/rules/');
|
||||
});
|
||||
|
||||
it('should handle file read errors', () => {
|
||||
// Mock file read to throw an error
|
||||
mockReadFileSync.mockImplementation(() => {
|
||||
throw new Error('File not found');
|
||||
});
|
||||
|
||||
// Call the actual function
|
||||
const result = convertRuleToProfileRule(
|
||||
'nonexistent.mdc',
|
||||
'target.md',
|
||||
kiloProfile
|
||||
);
|
||||
|
||||
// Verify the function failed gracefully
|
||||
expect(result).toBe(false);
|
||||
|
||||
// Verify writeFileSync was not called
|
||||
expect(mockWriteFileSync).not.toHaveBeenCalled();
|
||||
|
||||
// Verify error was logged
|
||||
expect(mockConsoleError).toHaveBeenCalledWith(
|
||||
'Error converting rule file: File not found'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle file write errors', () => {
|
||||
const testContent = 'test content';
|
||||
mockReadFileSync.mockReturnValue(testContent);
|
||||
|
||||
// Mock file write to throw an error
|
||||
mockWriteFileSync.mockImplementation(() => {
|
||||
throw new Error('Permission denied');
|
||||
});
|
||||
|
||||
// Call the actual function
|
||||
const result = convertRuleToProfileRule(
|
||||
'source.mdc',
|
||||
'target.md',
|
||||
kiloProfile
|
||||
);
|
||||
|
||||
// Verify the function failed gracefully
|
||||
expect(result).toBe(false);
|
||||
|
||||
// Verify error was logged
|
||||
expect(mockConsoleError).toHaveBeenCalledWith(
|
||||
'Error converting rule file: Permission denied'
|
||||
);
|
||||
});
|
||||
|
||||
it('should create target directory if it does not exist', () => {
|
||||
const testContent = 'test content';
|
||||
mockReadFileSync.mockReturnValue(testContent);
|
||||
|
||||
// Mock directory doesn't exist initially
|
||||
mockExistsSync.mockReturnValue(false);
|
||||
|
||||
// Call the actual function
|
||||
convertRuleToProfileRule(
|
||||
'source.mdc',
|
||||
'some/deep/path/target.md',
|
||||
kiloProfile
|
||||
);
|
||||
|
||||
// Verify directory creation was called
|
||||
expect(mockMkdirSync).toHaveBeenCalledWith('some/deep/path', {
|
||||
recursive: true
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -228,6 +228,11 @@ describe('Rule Transformer - General', () => {
|
||||
mcpConfigName: 'mcp.json',
|
||||
expectedPath: '.roo/mcp.json'
|
||||
},
|
||||
kilo: {
|
||||
mcpConfig: true,
|
||||
mcpConfigName: 'mcp.json',
|
||||
expectedPath: '.kilo/mcp.json'
|
||||
},
|
||||
trae: {
|
||||
mcpConfig: false,
|
||||
mcpConfigName: null,
|
||||
|
||||
134
tests/unit/progress/base-progress-tracker.test.js
Normal file
134
tests/unit/progress/base-progress-tracker.test.js
Normal file
@@ -0,0 +1,134 @@
|
||||
import { jest } from '@jest/globals';
|
||||
|
||||
// Mock cli-progress factory before importing BaseProgressTracker
|
||||
jest.unstable_mockModule(
|
||||
'../../../src/progress/cli-progress-factory.js',
|
||||
() => ({
|
||||
newMultiBar: jest.fn(() => ({
|
||||
create: jest.fn(() => ({
|
||||
update: jest.fn()
|
||||
})),
|
||||
stop: jest.fn()
|
||||
}))
|
||||
})
|
||||
);
|
||||
|
||||
const { newMultiBar } = await import(
|
||||
'../../../src/progress/cli-progress-factory.js'
|
||||
);
|
||||
const { BaseProgressTracker } = await import(
|
||||
'../../../src/progress/base-progress-tracker.js'
|
||||
);
|
||||
|
||||
describe('BaseProgressTracker', () => {
|
||||
let tracker;
|
||||
let mockMultiBar;
|
||||
let mockProgressBar;
|
||||
let mockTimeTokensBar;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.useFakeTimers();
|
||||
|
||||
// Setup mocks
|
||||
mockProgressBar = { update: jest.fn() };
|
||||
mockTimeTokensBar = { update: jest.fn() };
|
||||
mockMultiBar = {
|
||||
create: jest
|
||||
.fn()
|
||||
.mockReturnValueOnce(mockTimeTokensBar)
|
||||
.mockReturnValueOnce(mockProgressBar),
|
||||
stop: jest.fn()
|
||||
};
|
||||
newMultiBar.mockReturnValue(mockMultiBar);
|
||||
|
||||
tracker = new BaseProgressTracker({ numUnits: 10, unitName: 'task' });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
describe('cleanup', () => {
|
||||
it('should stop and clear timer interval', () => {
|
||||
tracker.start();
|
||||
expect(tracker._timerInterval).toBeTruthy();
|
||||
|
||||
tracker.cleanup();
|
||||
expect(tracker._timerInterval).toBeNull();
|
||||
});
|
||||
|
||||
it('should stop and null multibar reference', () => {
|
||||
tracker.start();
|
||||
expect(tracker.multibar).toBeTruthy();
|
||||
|
||||
tracker.cleanup();
|
||||
expect(mockMultiBar.stop).toHaveBeenCalled();
|
||||
expect(tracker.multibar).toBeNull();
|
||||
});
|
||||
|
||||
it('should null progress bar references', () => {
|
||||
tracker.start();
|
||||
expect(tracker.timeTokensBar).toBeTruthy();
|
||||
expect(tracker.progressBar).toBeTruthy();
|
||||
|
||||
tracker.cleanup();
|
||||
expect(tracker.timeTokensBar).toBeNull();
|
||||
expect(tracker.progressBar).toBeNull();
|
||||
});
|
||||
|
||||
it('should set finished state', () => {
|
||||
tracker.start();
|
||||
expect(tracker.isStarted).toBe(true);
|
||||
expect(tracker.isFinished).toBe(false);
|
||||
|
||||
tracker.cleanup();
|
||||
expect(tracker.isStarted).toBe(false);
|
||||
expect(tracker.isFinished).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle cleanup when multibar.stop throws error', () => {
|
||||
tracker.start();
|
||||
mockMultiBar.stop.mockImplementation(() => {
|
||||
throw new Error('Stop failed');
|
||||
});
|
||||
|
||||
expect(() => tracker.cleanup()).not.toThrow();
|
||||
expect(tracker.multibar).toBeNull();
|
||||
});
|
||||
|
||||
it('should be safe to call multiple times', () => {
|
||||
tracker.start();
|
||||
|
||||
tracker.cleanup();
|
||||
tracker.cleanup();
|
||||
tracker.cleanup();
|
||||
|
||||
expect(mockMultiBar.stop).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should be safe to call without starting', () => {
|
||||
expect(() => tracker.cleanup()).not.toThrow();
|
||||
expect(tracker.multibar).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('stop vs cleanup', () => {
|
||||
it('stop should call cleanup and null multibar reference', () => {
|
||||
tracker.start();
|
||||
tracker.stop();
|
||||
|
||||
// stop() now calls cleanup() which nulls the multibar
|
||||
expect(tracker.multibar).toBeNull();
|
||||
expect(tracker.isFinished).toBe(true);
|
||||
});
|
||||
|
||||
it('cleanup should null multibar preventing getSummary', () => {
|
||||
tracker.start();
|
||||
tracker.cleanup();
|
||||
|
||||
expect(tracker.multibar).toBeNull();
|
||||
expect(tracker.isFinished).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,7 @@
|
||||
* Tests for the add-task.js module
|
||||
*/
|
||||
import { jest } from '@jest/globals';
|
||||
import { hasCodebaseAnalysis } from '../../../../../scripts/modules/config-manager.js';
|
||||
|
||||
// Mock the dependencies before importing the module under test
|
||||
jest.unstable_mockModule('../../../../../scripts/modules/utils.js', () => ({
|
||||
@@ -99,7 +100,8 @@ jest.unstable_mockModule(
|
||||
jest.unstable_mockModule(
|
||||
'../../../../../scripts/modules/config-manager.js',
|
||||
() => ({
|
||||
getDefaultPriority: jest.fn(() => 'medium')
|
||||
getDefaultPriority: jest.fn(() => 'medium'),
|
||||
hasCodebaseAnalysis: jest.fn(() => false)
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -79,6 +79,38 @@ jest.unstable_mockModule(
|
||||
totalCost: 0.012414,
|
||||
currency: 'USD'
|
||||
}
|
||||
}),
|
||||
streamTextService: jest.fn().mockResolvedValue({
|
||||
mainResult: async function* () {
|
||||
yield '{"tasks":[';
|
||||
yield '{"id":1,"title":"Test Task","priority":"high"}';
|
||||
yield ']}';
|
||||
},
|
||||
telemetryData: {
|
||||
timestamp: new Date().toISOString(),
|
||||
userId: '1234567890',
|
||||
commandName: 'analyze-complexity',
|
||||
modelUsed: 'claude-3-5-sonnet',
|
||||
providerName: 'anthropic',
|
||||
inputTokens: 1000,
|
||||
outputTokens: 500,
|
||||
totalTokens: 1500,
|
||||
totalCost: 0.012414,
|
||||
currency: 'USD'
|
||||
}
|
||||
}),
|
||||
streamObjectService: jest.fn().mockImplementation(async () => {
|
||||
return {
|
||||
get partialObjectStream() {
|
||||
return (async function* () {
|
||||
yield { tasks: [] };
|
||||
yield { tasks: [{ id: 1, title: 'Test Task', priority: 'high' }] };
|
||||
})();
|
||||
},
|
||||
object: Promise.resolve({
|
||||
tasks: [{ id: 1, title: 'Test Task', priority: 'high' }]
|
||||
})
|
||||
};
|
||||
})
|
||||
})
|
||||
);
|
||||
@@ -155,7 +187,8 @@ jest.unstable_mockModule(
|
||||
// Additional functions
|
||||
getAllProviders: jest.fn(() => ['anthropic', 'openai', 'perplexity']),
|
||||
getVertexProjectId: jest.fn(() => undefined),
|
||||
getVertexLocation: jest.fn(() => undefined)
|
||||
getVertexLocation: jest.fn(() => undefined),
|
||||
hasCodebaseAnalysis: jest.fn(() => false)
|
||||
})
|
||||
);
|
||||
|
||||
@@ -189,9 +222,8 @@ const { readJSON, writeJSON, log, CONFIG, findTaskById } = await import(
|
||||
'../../../../../scripts/modules/utils.js'
|
||||
);
|
||||
|
||||
const { generateObjectService, generateTextService } = await import(
|
||||
'../../../../../scripts/modules/ai-services-unified.js'
|
||||
);
|
||||
const { generateObjectService, generateTextService, streamTextService } =
|
||||
await import('../../../../../scripts/modules/ai-services-unified.js');
|
||||
|
||||
const fs = await import('fs');
|
||||
|
||||
|
||||
@@ -178,6 +178,24 @@ jest.unstable_mockModule(
|
||||
});
|
||||
}
|
||||
}),
|
||||
streamTextService: jest.fn().mockResolvedValue({
|
||||
mainResult: async function* () {
|
||||
yield '{"tasks":[';
|
||||
yield '{"id":1,"title":"Test Task","priority":"high"}';
|
||||
yield ']}';
|
||||
},
|
||||
telemetryData: {
|
||||
timestamp: new Date().toISOString(),
|
||||
commandName: 'analyze-complexity',
|
||||
modelUsed: 'claude-3-5-sonnet',
|
||||
providerName: 'anthropic',
|
||||
inputTokens: 1000,
|
||||
outputTokens: 500,
|
||||
totalTokens: 1500,
|
||||
totalCost: 0.012414,
|
||||
currency: 'USD'
|
||||
}
|
||||
}),
|
||||
generateObjectService: jest.fn().mockResolvedValue({
|
||||
mainResult: {
|
||||
object: {
|
||||
@@ -283,7 +301,8 @@ jest.unstable_mockModule(
|
||||
// Additional functions
|
||||
getAllProviders: jest.fn(() => ['anthropic', 'openai', 'perplexity']),
|
||||
getVertexProjectId: jest.fn(() => undefined),
|
||||
getVertexLocation: jest.fn(() => undefined)
|
||||
getVertexLocation: jest.fn(() => undefined),
|
||||
hasCodebaseAnalysis: jest.fn(() => false)
|
||||
})
|
||||
);
|
||||
|
||||
@@ -402,7 +421,7 @@ const { readJSON, writeJSON, getTagAwareFilePath } = await import(
|
||||
'../../../../../scripts/modules/utils.js'
|
||||
);
|
||||
|
||||
const { generateTextService } = await import(
|
||||
const { generateTextService, streamTextService } = await import(
|
||||
'../../../../../scripts/modules/ai-services-unified.js'
|
||||
);
|
||||
|
||||
|
||||
@@ -125,7 +125,8 @@ jest.unstable_mockModule(
|
||||
getDebugFlag: jest.fn(() => false),
|
||||
getDefaultNumTasks: jest.fn(() => 10),
|
||||
getMainProvider: jest.fn(() => 'openai'),
|
||||
getResearchProvider: jest.fn(() => 'perplexity')
|
||||
getResearchProvider: jest.fn(() => 'perplexity'),
|
||||
hasCodebaseAnalysis: jest.fn(() => false)
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user