Compare commits
4 Commits
ralph/chor
...
task-maste
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd90b4d65f | ||
|
|
33259cc4f8 | ||
|
|
1917e6a01a | ||
|
|
35f3e71d2d |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"task-master-ai": patch
|
||||
---
|
||||
|
||||
Improve `analyze-complexity` cli docs and `--research` flag documentation
|
||||
@@ -8,8 +8,12 @@
|
||||
],
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"linked": [
|
||||
["task-master-ai", "@tm/cli", "@tm/core"]
|
||||
],
|
||||
"access": "public",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": [
|
||||
"docs"
|
||||
]
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
"task-master-ai": minor
|
||||
---
|
||||
|
||||
Add Cursor IDE custom slash command support
|
||||
|
||||
Expose Task Master commands as Cursor slash commands by copying assets/claude/commands to .cursor/commands on profile add and cleaning up on remove.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"task-master-ai": patch
|
||||
---
|
||||
|
||||
Change parent task back to "pending" when all subtasks are in "pending" state
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"task-master-ai": minor
|
||||
---
|
||||
|
||||
Added api keys page on docs website: docs.task-master.dev/getting-started/api-keys
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
"task-master-ai": minor
|
||||
---
|
||||
|
||||
Move to AI SDK v5:
|
||||
|
||||
- Works better with claude-code and gemini-cli as ai providers
|
||||
- Improved openai model family compatibility
|
||||
- Migrate ollama provider to v2
|
||||
- Closes #1223, #1013, #1161, #1174
|
||||
@@ -1,13 +0,0 @@
|
||||
---
|
||||
"task-master-ai": minor
|
||||
---
|
||||
|
||||
Enhanced Roo Code profile with MCP timeout configuration for improved reliability during long-running AI operations. The Roo profile now automatically configures a 300-second timeout for MCP server operations, preventing timeouts during complex tasks like `parse-prd`, `expand-all`, `analyze-complexity`, and `research` operations. This change also replaces static MCP configuration files with programmatic generation for better maintainability.
|
||||
|
||||
**What's New:**
|
||||
- 300-second timeout for MCP operations (up from default 60 seconds)
|
||||
- Programmatic MCP configuration generation (replaces static asset files)
|
||||
- Enhanced reliability for AI-powered operations
|
||||
- Consistent with other AI coding assistant profiles
|
||||
|
||||
**Migration:** No user action required - existing Roo Code installations will automatically receive the enhanced MCP configuration on next initialization.
|
||||
5
.changeset/moody-oranges-slide.md
Normal file
5
.changeset/moody-oranges-slide.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"task-master-ai": minor
|
||||
---
|
||||
|
||||
Test out the RC
|
||||
5
.changeset/odd-otters-tan.md
Normal file
5
.changeset/odd-otters-tan.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tm/cli": minor
|
||||
---
|
||||
|
||||
testing this stuff out to see how the release candidate works with monorepo
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"task-master-ai": patch
|
||||
---
|
||||
|
||||
Fix Claude Code settings validation for pathToClaudeCodeExecutable
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"task-master-ai": patch
|
||||
---
|
||||
|
||||
Fix sonar deep research model failing, should be called `sonar-deep-research`
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"task-master-ai": minor
|
||||
---
|
||||
|
||||
Upgrade grok-cli ai provider to ai sdk v5
|
||||
5
.changeset/wild-ears-look.md
Normal file
5
.changeset/wild-ears-look.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"extension": minor
|
||||
---
|
||||
|
||||
Added a Start Build button to the VSCODE Task Properties Right Panel
|
||||
@@ -2,7 +2,7 @@
|
||||
"mcpServers": {
|
||||
"task-master-ai": {
|
||||
"command": "node",
|
||||
"args": ["./dist/mcp-server.js"],
|
||||
"args": ["./mcp-server/server.js"],
|
||||
"env": {
|
||||
"ANTHROPIC_API_KEY": "ANTHROPIC_API_KEY_HERE",
|
||||
"PERPLEXITY_API_KEY": "PERPLEXITY_API_KEY_HERE",
|
||||
|
||||
157
.github/scripts/parse-metrics.mjs
vendored
157
.github/scripts/parse-metrics.mjs
vendored
@@ -1,157 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { readFileSync, existsSync, writeFileSync } from 'fs';
|
||||
|
||||
function parseMetricsTable(content, metricName) {
|
||||
const lines = content.split('\n');
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
// Match a markdown table row like: | Metric Name | value | ...
|
||||
const safeName = metricName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const re = new RegExp(`^\\|\\s*${safeName}\\s*\\|\\s*([^|]+)\\|?`);
|
||||
const match = line.match(re);
|
||||
if (match) {
|
||||
return match[1].trim() || 'N/A';
|
||||
}
|
||||
}
|
||||
return 'N/A';
|
||||
}
|
||||
|
||||
function parseCountMetric(content, metricName) {
|
||||
const result = parseMetricsTable(content, metricName);
|
||||
// Extract number from string, handling commas and spaces
|
||||
const numberMatch = result.toString().match(/[\d,]+/);
|
||||
if (numberMatch) {
|
||||
const number = parseInt(numberMatch[0].replace(/,/g, ''));
|
||||
return isNaN(number) ? 0 : number;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const metrics = {
|
||||
issues_created: 0,
|
||||
issues_closed: 0,
|
||||
prs_created: 0,
|
||||
prs_merged: 0,
|
||||
issue_avg_first_response: 'N/A',
|
||||
issue_avg_time_to_close: 'N/A',
|
||||
pr_avg_first_response: 'N/A',
|
||||
pr_avg_merge_time: 'N/A'
|
||||
};
|
||||
|
||||
// Parse issue metrics
|
||||
if (existsSync('issue_metrics.md')) {
|
||||
console.log('📄 Found issue_metrics.md, parsing...');
|
||||
const issueContent = readFileSync('issue_metrics.md', 'utf8');
|
||||
|
||||
metrics.issues_created = parseCountMetric(
|
||||
issueContent,
|
||||
'Total number of items created'
|
||||
);
|
||||
metrics.issues_closed = parseCountMetric(
|
||||
issueContent,
|
||||
'Number of items closed'
|
||||
);
|
||||
metrics.issue_avg_first_response = parseMetricsTable(
|
||||
issueContent,
|
||||
'Time to first response'
|
||||
);
|
||||
metrics.issue_avg_time_to_close = parseMetricsTable(
|
||||
issueContent,
|
||||
'Time to close'
|
||||
);
|
||||
} else {
|
||||
console.warn('[parse-metrics] issue_metrics.md not found; using defaults.');
|
||||
}
|
||||
|
||||
// Parse PR created metrics
|
||||
if (existsSync('pr_created_metrics.md')) {
|
||||
console.log('📄 Found pr_created_metrics.md, parsing...');
|
||||
const prCreatedContent = readFileSync('pr_created_metrics.md', 'utf8');
|
||||
|
||||
metrics.prs_created = parseCountMetric(
|
||||
prCreatedContent,
|
||||
'Total number of items created'
|
||||
);
|
||||
metrics.pr_avg_first_response = parseMetricsTable(
|
||||
prCreatedContent,
|
||||
'Time to first response'
|
||||
);
|
||||
} else {
|
||||
console.warn(
|
||||
'[parse-metrics] pr_created_metrics.md not found; using defaults.'
|
||||
);
|
||||
}
|
||||
|
||||
// Parse PR merged metrics (for more accurate merge data)
|
||||
if (existsSync('pr_merged_metrics.md')) {
|
||||
console.log('📄 Found pr_merged_metrics.md, parsing...');
|
||||
const prMergedContent = readFileSync('pr_merged_metrics.md', 'utf8');
|
||||
|
||||
metrics.prs_merged = parseCountMetric(
|
||||
prMergedContent,
|
||||
'Total number of items created'
|
||||
);
|
||||
// For merged PRs, "Time to close" is actually time to merge
|
||||
metrics.pr_avg_merge_time = parseMetricsTable(
|
||||
prMergedContent,
|
||||
'Time to close'
|
||||
);
|
||||
} else {
|
||||
console.warn(
|
||||
'[parse-metrics] pr_merged_metrics.md not found; falling back to pr_metrics.md.'
|
||||
);
|
||||
// Fallback: try old pr_metrics.md if it exists
|
||||
if (existsSync('pr_metrics.md')) {
|
||||
console.log('📄 Falling back to pr_metrics.md...');
|
||||
const prContent = readFileSync('pr_metrics.md', 'utf8');
|
||||
|
||||
const mergedCount = parseCountMetric(prContent, 'Number of items merged');
|
||||
metrics.prs_merged =
|
||||
mergedCount || parseCountMetric(prContent, 'Number of items closed');
|
||||
|
||||
const maybeMergeTime = parseMetricsTable(
|
||||
prContent,
|
||||
'Average time to merge'
|
||||
);
|
||||
metrics.pr_avg_merge_time =
|
||||
maybeMergeTime !== 'N/A'
|
||||
? maybeMergeTime
|
||||
: parseMetricsTable(prContent, 'Time to close');
|
||||
} else {
|
||||
console.warn('[parse-metrics] pr_metrics.md not found; using defaults.');
|
||||
}
|
||||
}
|
||||
|
||||
// Output for GitHub Actions
|
||||
const output = Object.entries(metrics)
|
||||
.map(([key, value]) => `${key}=${value}`)
|
||||
.join('\n');
|
||||
|
||||
// Always output to stdout for debugging
|
||||
console.log('\n=== FINAL METRICS ===');
|
||||
Object.entries(metrics).forEach(([key, value]) => {
|
||||
console.log(`${key}: ${value}`);
|
||||
});
|
||||
|
||||
// Write to GITHUB_OUTPUT if in GitHub Actions
|
||||
if (process.env.GITHUB_OUTPUT) {
|
||||
try {
|
||||
writeFileSync(process.env.GITHUB_OUTPUT, output + '\n', { flag: 'a' });
|
||||
console.log(
|
||||
`\nSuccessfully wrote metrics to ${process.env.GITHUB_OUTPUT}`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(`Failed to write to GITHUB_OUTPUT: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
'\nNo GITHUB_OUTPUT environment variable found, skipping file write'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -6,6 +6,9 @@ on:
|
||||
- main
|
||||
- next
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- next
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
@@ -89,9 +92,6 @@ jobs:
|
||||
env:
|
||||
NODE_ENV: production
|
||||
FORCE_COLOR: 1
|
||||
TM_PUBLIC_BASE_DOMAIN: ${{ secrets.TM_PUBLIC_BASE_DOMAIN }}
|
||||
TM_PUBLIC_SUPABASE_URL: ${{ secrets.TM_PUBLIC_SUPABASE_URL }}
|
||||
TM_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.TM_PUBLIC_SUPABASE_ANON_KEY }}
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
5
.github/workflows/extension-ci.yml
vendored
5
.github/workflows/extension-ci.yml
vendored
@@ -41,7 +41,8 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
|
||||
- name: Install Monorepo Dependencies
|
||||
- name: Install Extension Dependencies
|
||||
working-directory: apps/extension
|
||||
run: npm ci
|
||||
timeout-minutes: 5
|
||||
|
||||
@@ -67,6 +68,7 @@ jobs:
|
||||
${{ runner.os }}-node-
|
||||
|
||||
- name: Install if cache miss
|
||||
working-directory: apps/extension
|
||||
run: npm ci
|
||||
timeout-minutes: 3
|
||||
|
||||
@@ -98,6 +100,7 @@ jobs:
|
||||
${{ runner.os }}-node-
|
||||
|
||||
- name: Install if cache miss
|
||||
working-directory: apps/extension
|
||||
run: npm ci
|
||||
timeout-minutes: 3
|
||||
|
||||
|
||||
3
.github/workflows/extension-release.yml
vendored
3
.github/workflows/extension-release.yml
vendored
@@ -31,7 +31,8 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
|
||||
- name: Install Monorepo Dependencies
|
||||
- name: Install Extension Dependencies
|
||||
working-directory: apps/extension
|
||||
run: npm ci
|
||||
timeout-minutes: 5
|
||||
|
||||
|
||||
8
.github/workflows/pre-release.yml
vendored
8
.github/workflows/pre-release.yml
vendored
@@ -65,19 +65,11 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Run format
|
||||
run: npm run format
|
||||
env:
|
||||
FORCE_COLOR: 1
|
||||
|
||||
- name: Build packages
|
||||
run: npm run turbo:build
|
||||
env:
|
||||
NODE_ENV: production
|
||||
FORCE_COLOR: 1
|
||||
TM_PUBLIC_BASE_DOMAIN: ${{ secrets.TM_PUBLIC_BASE_DOMAIN }}
|
||||
TM_PUBLIC_SUPABASE_URL: ${{ secrets.TM_PUBLIC_SUPABASE_URL }}
|
||||
TM_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.TM_PUBLIC_SUPABASE_ANON_KEY }}
|
||||
|
||||
- name: Create Release Candidate Pull Request or Publish Release Candidate to npm
|
||||
uses: changesets/action@v1
|
||||
|
||||
5
.github/workflows/release.yml
vendored
5
.github/workflows/release.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: "npm"
|
||||
cache: 'npm'
|
||||
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v4
|
||||
@@ -46,9 +46,6 @@ jobs:
|
||||
env:
|
||||
NODE_ENV: production
|
||||
FORCE_COLOR: 1
|
||||
TM_PUBLIC_BASE_DOMAIN: ${{ secrets.TM_PUBLIC_BASE_DOMAIN }}
|
||||
TM_PUBLIC_SUPABASE_URL: ${{ secrets.TM_PUBLIC_SUPABASE_URL }}
|
||||
TM_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.TM_PUBLIC_SUPABASE_ANON_KEY }}
|
||||
|
||||
- name: Create Release Pull Request or Publish to npm
|
||||
uses: changesets/action@v1
|
||||
|
||||
86
.github/workflows/weekly-metrics-discord.yml
vendored
86
.github/workflows/weekly-metrics-discord.yml
vendored
@@ -8,7 +8,7 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: read
|
||||
issues: write
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
@@ -17,25 +17,15 @@ jobs:
|
||||
env:
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_METRICS_WEBHOOK }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Get dates for last 14 days
|
||||
- name: Get dates for last week
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
# Last 14 days
|
||||
first_day=$(date -d "14 days ago" +%Y-%m-%d)
|
||||
# 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
|
||||
echo "date_range=Past 14 days ($first_day to $last_day)" >> $GITHUB_ENV
|
||||
|
||||
- name: Generate issue metrics
|
||||
uses: github/issue-metrics@v3
|
||||
@@ -44,39 +34,40 @@ jobs:
|
||||
SEARCH_QUERY: "repo:${{ github.repository }} is:issue created:${{ env.first_day }}..${{ env.last_day }}"
|
||||
HIDE_TIME_TO_ANSWER: true
|
||||
HIDE_LABEL_METRICS: false
|
||||
OUTPUT_FILE: issue_metrics.md
|
||||
|
||||
- name: Generate PR created metrics
|
||||
- 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_created_metrics.md
|
||||
|
||||
- name: Generate PR merged metrics
|
||||
uses: github/issue-metrics@v3
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SEARCH_QUERY: "repo:${{ github.repository }} is:pr is:merged merged:${{ env.first_day }}..${{ env.last_day }}"
|
||||
OUTPUT_FILE: pr_merged_metrics.md
|
||||
|
||||
- name: Debug generated metrics
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
echo "Listing markdown files in workspace:"
|
||||
ls -la *.md || true
|
||||
for f in issue_metrics.md pr_created_metrics.md pr_merged_metrics.md; do
|
||||
if [ -f "$f" ]; then
|
||||
echo "== $f (first 10 lines) =="
|
||||
head -n 10 "$f"
|
||||
else
|
||||
echo "Missing $f"
|
||||
fi
|
||||
done
|
||||
OUTPUT_FILE: pr_metrics.md
|
||||
|
||||
- name: Parse metrics
|
||||
id: metrics
|
||||
run: node .github/scripts/parse-metrics.mjs
|
||||
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
|
||||
@@ -87,22 +78,19 @@ jobs:
|
||||
title: "📊 Weekly Metrics Report"
|
||||
description: |
|
||||
**${{ env.week_of }}**
|
||||
*${{ env.date_range }}*
|
||||
|
||||
|
||||
**🎯 Issues**
|
||||
• Created: ${{ steps.metrics.outputs.issues_created }}
|
||||
• Closed: ${{ steps.metrics.outputs.issues_closed }}
|
||||
• Avg Response Time: ${{ steps.metrics.outputs.issue_avg_first_response }}
|
||||
• Avg Time to Close: ${{ steps.metrics.outputs.issue_avg_time_to_close }}
|
||||
|
||||
|
||||
**🔀 Pull Requests**
|
||||
• Created: ${{ steps.metrics.outputs.prs_created }}
|
||||
• Merged: ${{ steps.metrics.outputs.prs_merged }}
|
||||
• Avg Response Time: ${{ steps.metrics.outputs.pr_avg_first_response }}
|
||||
• Avg Time to Merge: ${{ steps.metrics.outputs.pr_avg_merge_time }}
|
||||
|
||||
**📈 Visual Analytics**
|
||||
https://repobeats.axiom.co/api/embed/b439f28f0ab5bd7a2da19505355693cd2c55bfd4.svg
|
||||
|
||||
**⏱️ 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
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"mcpServers": {
|
||||
"task-master-ai": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "task-master-ai"],
|
||||
"args": ["-y", "--package=task-master-ai", "task-master-ai"],
|
||||
"env": {
|
||||
"ANTHROPIC_API_KEY": "YOUR_ANTHROPIC_API_KEY_HERE",
|
||||
"PERPLEXITY_API_KEY": "YOUR_PERPLEXITY_API_KEY_HERE",
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@manypkg/get-packages@1.1.3/schema.json",
|
||||
"defaultBranch": "main",
|
||||
"ignoredRules": ["ROOT_HAS_DEPENDENCIES", "INTERNAL_MISMATCH"],
|
||||
"ignoredPackages": ["@tm/core", "@tm/cli", "@tm/build-config"]
|
||||
}
|
||||
@@ -85,7 +85,7 @@ Task Master provides an MCP server that Claude Code can connect to. Configure in
|
||||
"mcpServers": {
|
||||
"task-master-ai": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "task-master-ai"],
|
||||
"args": ["-y", "--package=task-master-ai", "task-master-ai"],
|
||||
"env": {
|
||||
"ANTHROPIC_API_KEY": "your_key_here",
|
||||
"PERPLEXITY_API_KEY": "your_key_here",
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
"models": {
|
||||
"main": {
|
||||
"provider": "anthropic",
|
||||
"modelId": "claude-sonnet-4-20250514",
|
||||
"maxTokens": 64000,
|
||||
"modelId": "claude-3-7-sonnet-20250219",
|
||||
"maxTokens": 120000,
|
||||
"temperature": 0.2
|
||||
},
|
||||
"research": {
|
||||
@@ -14,8 +14,8 @@
|
||||
},
|
||||
"fallback": {
|
||||
"provider": "anthropic",
|
||||
"modelId": "claude-3-7-sonnet-20250219",
|
||||
"maxTokens": 120000,
|
||||
"modelId": "claude-3-5-sonnet-20241022",
|
||||
"maxTokens": 8192,
|
||||
"temperature": 0.2
|
||||
}
|
||||
},
|
||||
@@ -29,15 +29,9 @@
|
||||
"ollamaBaseURL": "http://localhost:11434/api",
|
||||
"bedrockBaseURL": "https://bedrock.us-east-1.amazonaws.com",
|
||||
"responseLanguage": "English",
|
||||
"enableCodebaseAnalysis": true,
|
||||
"userId": "1234567890",
|
||||
"azureBaseURL": "https://your-endpoint.azure.com/",
|
||||
"defaultTag": "master"
|
||||
},
|
||||
"claudeCode": {},
|
||||
"grokCli": {
|
||||
"timeout": 120000,
|
||||
"workingDirectory": null,
|
||||
"defaultModel": "grok-4-latest"
|
||||
}
|
||||
"claudeCode": {}
|
||||
}
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
<context>
|
||||
# Overview
|
||||
Add a new CLI command: `task-master start <task_id>` (alias: `tm start <task_id>`). This command hard-codes `claude-code` as the executor, fetches task details, builds a standardized prompt, runs claude-code, shows the result, checks for git changes, and auto-marks the task as done if successful.
|
||||
|
||||
We follow the Commander class pattern, reuse task retrieval from `show` command flow. Extremely minimal for 1-hour hackathon timeline.
|
||||
|
||||
# Core Features
|
||||
- `start` command (Commander class style)
|
||||
- Hard-coded executor: `claude-code`
|
||||
- Standardized prompt designed for minimal changes following existing patterns
|
||||
- Shows claude-code output (no streaming)
|
||||
- Git status check for success detection
|
||||
- Auto-mark task done if successful
|
||||
|
||||
# User Experience
|
||||
```
|
||||
task-master start 12
|
||||
```
|
||||
1) Fetches Task #12 details
|
||||
2) Builds standardized prompt with task context
|
||||
3) Runs claude-code with the prompt
|
||||
4) Shows output
|
||||
5) Checks git status for changes
|
||||
6) Auto-marks task done if changes detected
|
||||
</context>
|
||||
|
||||
<PRD>
|
||||
# Technical Architecture
|
||||
|
||||
- Command pattern:
|
||||
- Create `apps/cli/src/commands/start.command.ts` modeled on [list.command.ts](mdc:apps/cli/src/commands/list.command.ts) and task lookup from [show.command.ts](mdc:apps/cli/src/commands/show.command.ts)
|
||||
|
||||
- Task retrieval:
|
||||
- Use `@tm/core` via `createTaskMasterCore` to get task by ID
|
||||
- Extract: id, title, description, details
|
||||
|
||||
- Executor (ultra-simple approach):
|
||||
- Execute `claude "full prompt here"` command directly
|
||||
- The prompt tells Claude to first run `tm show <task_id>` to get task details
|
||||
- Then tells Claude to implement the code changes
|
||||
- This opens Claude CLI interface naturally in the current terminal
|
||||
- No subprocess management needed - just execute the command
|
||||
|
||||
- Execution flow:
|
||||
1) Validate `<task_id>` exists; exit with error if not
|
||||
2) Build standardized prompt that includes instructions to run `tm show <task_id>`
|
||||
3) Execute `claude "prompt"` command directly in terminal
|
||||
4) Claude CLI opens, runs `tm show`, then implements changes
|
||||
5) After Claude session ends, run `git status --porcelain` to detect changes
|
||||
6) If changes detected, auto-run `task-master set-status --id=<task_id> --status=done`
|
||||
|
||||
- Success criteria:
|
||||
- Success = exit code 0 AND git shows modified/created files
|
||||
- Print changed file paths; warn if no changes detected
|
||||
|
||||
# Development Roadmap
|
||||
|
||||
MVP (ship in ~1 hour):
|
||||
1) Implement `start.command.ts` (Commander class), parse `<task_id>`
|
||||
2) Validate task exists via tm-core
|
||||
3) Build prompt that tells Claude to run `tm show <task_id>` then implement
|
||||
4) Execute `claude "prompt"` command, then check git status and auto-mark done
|
||||
|
||||
# Risks and Mitigations
|
||||
- Executor availability: Error clearly if `claude-code` provider fails
|
||||
- False success: Git-change heuristic acceptable for hackathon MVP
|
||||
|
||||
# Appendix
|
||||
|
||||
**Standardized Prompt Template:**
|
||||
```
|
||||
You are an AI coding assistant with access to this repository's codebase.
|
||||
|
||||
First, run this command to get the task details:
|
||||
tm show <task_id>
|
||||
|
||||
Then implement the task with these requirements:
|
||||
- Make the SMALLEST number of code changes possible
|
||||
- Follow ALL existing patterns in the codebase (you have access to analyze the code)
|
||||
- Do NOT over-engineer the solution
|
||||
- Use existing files/functions/patterns wherever possible
|
||||
- When complete, print: COMPLETED: <brief summary of changes>
|
||||
|
||||
Begin by running tm show <task_id> to understand what needs to be implemented.
|
||||
```
|
||||
|
||||
**Key References:**
|
||||
- [list.command.ts](mdc:apps/cli/src/commands/list.command.ts) - Command structure
|
||||
- [show.command.ts](mdc:apps/cli/src/commands/show.command.ts) - Task validation
|
||||
- Node.js `child_process.exec()` - For executing `claude "prompt"` command
|
||||
</PRD>
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"currentTag": "master",
|
||||
"lastSwitched": "2025-09-12T22:25:27.535Z",
|
||||
"lastSwitched": "2025-08-27T21:03:20.550Z",
|
||||
"branchTagMapping": {
|
||||
"v017-adds": "v017-adds",
|
||||
"next": "next"
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
# Task ID: 1
|
||||
# Title: Create start command class structure
|
||||
# Status: pending
|
||||
# Dependencies: None
|
||||
# Priority: high
|
||||
# Description: Create the basic structure for the start command following the Commander class pattern
|
||||
# Details:
|
||||
Create a new file `apps/cli/src/commands/start.command.ts` based on the existing list.command.ts pattern. Implement the command class with proper command registration, description, and argument handling for the task_id parameter. The class should extend the base Command class and implement the required methods.
|
||||
|
||||
Example structure:
|
||||
```typescript
|
||||
import { Command } from 'commander';
|
||||
import { BaseCommand } from './base.command';
|
||||
|
||||
export class StartCommand extends BaseCommand {
|
||||
public register(program: Command): void {
|
||||
program
|
||||
.command('start')
|
||||
.alias('tm start')
|
||||
.description('Start implementing a task using claude-code')
|
||||
.argument('<task_id>', 'ID of the task to start')
|
||||
.action(async (taskId: string) => {
|
||||
await this.execute(taskId);
|
||||
});
|
||||
}
|
||||
|
||||
public async execute(taskId: string): Promise<void> {
|
||||
// Implementation will be added in subsequent tasks
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# Test Strategy:
|
||||
Verify the command registers correctly by running the CLI with --help and checking that the start command appears with proper description and arguments. Test the basic structure by ensuring the command can be invoked without errors.
|
||||
@@ -1,26 +0,0 @@
|
||||
# Task ID: 2
|
||||
# Title: Register start command in CLI
|
||||
# Status: pending
|
||||
# Dependencies: 7
|
||||
# Priority: high
|
||||
# Description: Register the start command in the CLI application
|
||||
# Details:
|
||||
Update the CLI application to register the new start command. This involves importing the StartCommand class and adding it to the commands array in the CLI initialization.
|
||||
|
||||
In `apps/cli/src/index.ts` or the appropriate file where commands are registered:
|
||||
|
||||
```typescript
|
||||
import { StartCommand } from './commands/start.command';
|
||||
|
||||
// Add StartCommand to the commands array
|
||||
const commands = [
|
||||
// ... existing commands
|
||||
new StartCommand(),
|
||||
];
|
||||
|
||||
// Register all commands
|
||||
commands.forEach(command => command.register(program));
|
||||
```
|
||||
|
||||
# Test Strategy:
|
||||
Verify the command is correctly registered by running the CLI with --help and checking that the start command appears in the list of available commands.
|
||||
@@ -1,32 +0,0 @@
|
||||
# Task ID: 3
|
||||
# Title: Create standardized prompt builder
|
||||
# Status: pending
|
||||
# Dependencies: 1
|
||||
# Priority: medium
|
||||
# Description: Implement a function to build the standardized prompt for claude-code based on the task details
|
||||
# Details:
|
||||
Create a function in the StartCommand class that builds the standardized prompt according to the template provided in the PRD. The prompt should include instructions for Claude to first run `tm show <task_id>` to get task details, and then implement the required changes.
|
||||
|
||||
```typescript
|
||||
private buildPrompt(taskId: string): string {
|
||||
return `You are an AI coding assistant with access to this repository's codebase.
|
||||
|
||||
First, run this command to get the task details:
|
||||
tm show ${taskId}
|
||||
|
||||
Then implement the task with these requirements:
|
||||
- Make the SMALLEST number of code changes possible
|
||||
- Follow ALL existing patterns in the codebase (you have access to analyze the code)
|
||||
- Do NOT over-engineer the solution
|
||||
- Use existing files/functions/patterns wherever possible
|
||||
- When complete, print: COMPLETED: <brief summary of changes>
|
||||
|
||||
Begin by running tm show ${taskId} to understand what needs to be implemented.`;
|
||||
}
|
||||
```
|
||||
<info added on 2025-09-12T02:40:01.812Z>
|
||||
The prompt builder function will handle task context retrieval by instructing Claude to use the task-master show command. This approach ensures Claude has access to all necessary task details before implementation begins. The command syntax "tm show ${taskId}" embedded in the prompt will direct Claude to first gather the complete task context, including description, requirements, and any existing implementation details, before proceeding with code changes.
|
||||
</info added on 2025-09-12T02:40:01.812Z>
|
||||
|
||||
# Test Strategy:
|
||||
Verify the prompt is correctly formatted by calling the function with a sample task ID and checking that the output matches the expected template with the task ID properly inserted.
|
||||
@@ -1,36 +0,0 @@
|
||||
# Task ID: 4
|
||||
# Title: Implement claude-code executor
|
||||
# Status: pending
|
||||
# Dependencies: 3
|
||||
# Priority: high
|
||||
# Description: Add functionality to execute the claude-code command with the built prompt
|
||||
# Details:
|
||||
Implement the functionality to execute the claude command with the built prompt. This should use Node.js child_process.exec() to run the command directly in the terminal.
|
||||
|
||||
```typescript
|
||||
import { exec } from 'child_process';
|
||||
|
||||
// Inside execute method, after task validation
|
||||
private async executeClaude(prompt: string): Promise<void> {
|
||||
console.log('Starting claude-code to implement the task...');
|
||||
|
||||
try {
|
||||
// Execute claude with the prompt
|
||||
const claudeCommand = `claude "${prompt.replace(/"/g, '\\"')}"`;
|
||||
|
||||
// Use execSync to wait for the command to complete
|
||||
const { execSync } = require('child_process');
|
||||
execSync(claudeCommand, { stdio: 'inherit' });
|
||||
|
||||
console.log('Claude session completed.');
|
||||
} catch (error) {
|
||||
console.error('Error executing claude-code:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then call this method from the execute method after building the prompt.
|
||||
|
||||
# Test Strategy:
|
||||
Test by running the command with a valid task ID and verifying that the claude command is executed with the correct prompt. Check that the command handles errors appropriately if claude-code is not available.
|
||||
@@ -1,49 +0,0 @@
|
||||
# Task ID: 7
|
||||
# Title: Integrate execution flow in start command
|
||||
# Status: pending
|
||||
# Dependencies: 3, 4
|
||||
# Priority: high
|
||||
# Description: Connect all the components to implement the complete execution flow for the start command
|
||||
# Details:
|
||||
Update the execute method in the StartCommand class to integrate all the components and implement the complete execution flow as described in the PRD:
|
||||
1. Validate task exists
|
||||
2. Build standardized prompt
|
||||
3. Execute claude-code
|
||||
4. Check git status for changes
|
||||
5. Auto-mark task as done if changes detected
|
||||
|
||||
```typescript
|
||||
public async execute(taskId: string): Promise<void> {
|
||||
// Validate task exists
|
||||
const core = await createTaskMasterCore();
|
||||
const task = await core.tasks.getById(parseInt(taskId, 10));
|
||||
|
||||
if (!task) {
|
||||
console.error(`Task with ID ${taskId} not found`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Build prompt
|
||||
const prompt = this.buildPrompt(taskId);
|
||||
|
||||
// Execute claude-code
|
||||
await this.executeClaude(prompt);
|
||||
|
||||
// Check git status
|
||||
const changedFiles = await this.checkGitChanges();
|
||||
|
||||
if (changedFiles.length > 0) {
|
||||
console.log('\nChanges detected in the following files:');
|
||||
changedFiles.forEach(file => console.log(`- ${file}`));
|
||||
|
||||
// Auto-mark task as done
|
||||
await this.markTaskAsDone(taskId);
|
||||
console.log(`\nTask ${taskId} completed successfully and marked as done.`);
|
||||
} else {
|
||||
console.warn('\nNo changes detected after claude-code execution. Task not marked as done.');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# Test Strategy:
|
||||
Test the complete execution flow by running the start command with a valid task ID and verifying that all steps are executed correctly. Test with both scenarios: when changes are detected and when no changes are detected.
|
||||
File diff suppressed because one or more lines are too long
114
CHANGELOG.md
114
CHANGELOG.md
@@ -1,119 +1,5 @@
|
||||
# task-master-ai
|
||||
|
||||
## 0.27.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#1254](https://github.com/eyaltoledano/claude-task-master/pull/1254) [`af53525`](https://github.com/eyaltoledano/claude-task-master/commit/af53525cbc660a595b67d4bb90d906911c71f45d) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fixed issue where `tm show` command could not find subtasks using dotted notation IDs (e.g., '8.1').
|
||||
- The command now properly searches within parent task subtasks and returns the correct subtask information.
|
||||
|
||||
## 0.27.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#1248](https://github.com/eyaltoledano/claude-task-master/pull/1248) [`044a7bf`](https://github.com/eyaltoledano/claude-task-master/commit/044a7bfc98049298177bc655cf341d7a8b6a0011) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fix set-status for subtasks:
|
||||
- Parent tasks are now set as `done` when subtasks are all `done`
|
||||
- Parent tasks are now set as `in-progress` when at least one subtask is `in-progress` or `done`
|
||||
|
||||
## 0.27.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#1232](https://github.com/eyaltoledano/claude-task-master/pull/1232) [`f487736`](https://github.com/eyaltoledano/claude-task-master/commit/f487736670ef8c484059f676293777eabb249c9e) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fix module not found for new 0.27.0 release
|
||||
|
||||
- [#1233](https://github.com/eyaltoledano/claude-task-master/pull/1233) [`c911608`](https://github.com/eyaltoledano/claude-task-master/commit/c911608f60454253f4e024b57ca84e5a5a53f65c) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fix Zed MCP configuration by adding required "source" property
|
||||
- Add "source": "custom" property to task-master-ai server in Zed settings.json
|
||||
|
||||
## 0.27.1-rc.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#1233](https://github.com/eyaltoledano/claude-task-master/pull/1233) [`1a18794`](https://github.com/eyaltoledano/claude-task-master/commit/1a1879483b86c118a4e46c02cbf4acebfcf6bcf9) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - One last testing final final
|
||||
|
||||
## 0.27.1-rc.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#1232](https://github.com/eyaltoledano/claude-task-master/pull/1232) [`f487736`](https://github.com/eyaltoledano/claude-task-master/commit/f487736670ef8c484059f676293777eabb249c9e) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fix module not found for new 0.27.0 release
|
||||
|
||||
## 0.27.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#1220](https://github.com/eyaltoledano/claude-task-master/pull/1220) [`4e12643`](https://github.com/eyaltoledano/claude-task-master/commit/4e126430a092fb54afb035514fb3d46115714f97) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - No longer need --package=task-master-ai in mcp server
|
||||
- A lot of users were having issues with Taskmaster and usually a simple fix was to remove --package from your mcp.json
|
||||
- we now bundle our whole package, so we no longer need the --package
|
||||
|
||||
- [#1200](https://github.com/eyaltoledano/claude-task-master/pull/1200) [`fce8414`](https://github.com/eyaltoledano/claude-task-master/commit/fce841490a9ebbf1801a42dd8a29397379cf1142) Thanks [@eyaltoledano](https://github.com/eyaltoledano)! - Add new `task-master start` command for automated task execution with Claude Code
|
||||
- You can now start working on tasks directly by running `task-master start <task-id>` which will automatically launch Claude Code with a comprehensive prompt containing all task details, implementation guidelines, and context.
|
||||
- `task-master start` will automatically detect next-task when no ID is provided.
|
||||
|
||||
- [#1200](https://github.com/eyaltoledano/claude-task-master/pull/1200) [`fce8414`](https://github.com/eyaltoledano/claude-task-master/commit/fce841490a9ebbf1801a42dd8a29397379cf1142) Thanks [@eyaltoledano](https://github.com/eyaltoledano)! - Move from javascript to typescript, not a full refactor but we now have a typescript environment and are moving our javascript commands slowly into typescript
|
||||
|
||||
- [#1200](https://github.com/eyaltoledano/claude-task-master/pull/1200) [`fce8414`](https://github.com/eyaltoledano/claude-task-master/commit/fce841490a9ebbf1801a42dd8a29397379cf1142) Thanks [@eyaltoledano](https://github.com/eyaltoledano)! - Add grok-cli as a provider with full codebase context support. You can now use Grok models (grok-2, grok-3, grok-4, etc.) with Task Master for AI operations that have access to your entire codebase context, enabling more informed task generation and PRD parsing.
|
||||
|
||||
## Setup Instructions
|
||||
1. **Get your Grok API key** from [console.x.ai](https://console.x.ai)
|
||||
2. **Set the environment variable**:
|
||||
```bash
|
||||
export GROK_CLI_API_KEY="your-api-key-here"
|
||||
```
|
||||
3. **Configure Task Master to use Grok**:
|
||||
```bash
|
||||
task-master models --set-main grok-beta
|
||||
# or
|
||||
task-master models --set-research grok-beta
|
||||
# or
|
||||
task-master models --set-fallback grok-beta
|
||||
```
|
||||
|
||||
## Key Features
|
||||
- **Full codebase context**: Grok models can analyze your entire project when generating tasks or parsing PRDs
|
||||
- **xAI model access**: Support for latest Grok models (grok-2, grok-3, grok-4, etc.)
|
||||
- **Code-aware task generation**: Create more accurate and contextual tasks based on your actual codebase
|
||||
- **Intelligent PRD parsing**: Parse requirements with understanding of your existing code structure
|
||||
|
||||
## Available Models
|
||||
- `grok-beta` - Latest Grok model with codebase context
|
||||
- `grok-vision-beta` - Grok with vision capabilities and codebase context
|
||||
|
||||
The Grok CLI provider integrates with xAI's Grok models via grok-cli and can also use the local Grok CLI configuration file (`~/.grok/user-settings.json`) if available.
|
||||
|
||||
## Credits
|
||||
|
||||
Built using the [grok-cli](https://github.com/superagent-ai/grok-cli) by Superagent AI for seamless integration with xAI's Grok models.
|
||||
|
||||
- [#1225](https://github.com/eyaltoledano/claude-task-master/pull/1225) [`a621ff0`](https://github.com/eyaltoledano/claude-task-master/commit/a621ff05eafb51a147a9aabd7b37ddc0e45b0869) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Improve taskmaster ai provider defaults
|
||||
- moving from main anthropic 3.7 to anthropic sonnet 4
|
||||
- moving from fallback anthropic 3.5 to anthropic 3.7
|
||||
|
||||
- [#1217](https://github.com/eyaltoledano/claude-task-master/pull/1217) [`e6de285`](https://github.com/eyaltoledano/claude-task-master/commit/e6de285ceacb0a397e952a63435cd32a9c731515) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - @tm/cli: add auto-update functionality to every command
|
||||
|
||||
- [#1200](https://github.com/eyaltoledano/claude-task-master/pull/1200) [`fce8414`](https://github.com/eyaltoledano/claude-task-master/commit/fce841490a9ebbf1801a42dd8a29397379cf1142) Thanks [@eyaltoledano](https://github.com/eyaltoledano)! - Fix Grok model configuration validation and update deprecated Claude fallback model. Grok models now properly support their full 131K token capacity, and the fallback model has been upgraded to Claude Sonnet 4 for better performance and future compatibility.
|
||||
|
||||
## 0.27.0-rc.2
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#1217](https://github.com/eyaltoledano/claude-task-master/pull/1217) [`e6de285`](https://github.com/eyaltoledano/claude-task-master/commit/e6de285ceacb0a397e952a63435cd32a9c731515) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - @tm/cli: add auto-update functionality to every command
|
||||
|
||||
## 0.27.0-rc.1
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [`255b9f0`](https://github.com/eyaltoledano/claude-task-master/commit/255b9f0334555b0063280abde701445cd62fa11b) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Testing one more pre-release iteration
|
||||
|
||||
## 0.27.0-rc.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#1213](https://github.com/eyaltoledano/claude-task-master/pull/1213) [`137ef36`](https://github.com/eyaltoledano/claude-task-master/commit/137ef362789a9cdfdb1925e35e0438c1fa6c69ee) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Test out the RC
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`137ef36`](https://github.com/eyaltoledano/claude-task-master/commit/137ef362789a9cdfdb1925e35e0438c1fa6c69ee)]:
|
||||
- @tm/cli@0.27.0-rc.0
|
||||
|
||||
## 0.26.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
22
README.md
22
README.md
@@ -60,19 +60,6 @@ The following documentation is also available in the `docs` directory:
|
||||
|
||||
> **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.
|
||||
|
||||
#### Claude Code Quick Install
|
||||
|
||||
For Claude Code users:
|
||||
|
||||
```bash
|
||||
claude mcp add taskmaster-ai -- npx -y task-master-ai
|
||||
```
|
||||
|
||||
Don't forget to add your API keys to the configuration:
|
||||
- in the root .env of your Project
|
||||
- in the "env" section of your mcp config for taskmaster-ai
|
||||
|
||||
|
||||
## Requirements
|
||||
|
||||
Taskmaster utilizes AI across several commands, and those require a separate API key. You can use a variety of models from different AI providers provided you add your API keys. For example, if you want to use Claude 3.7, you'll need an Anthropic API key.
|
||||
@@ -105,18 +92,17 @@ MCP (Model Control Protocol) lets you run Task Master directly from your editor.
|
||||
| | Project | `<project_folder>/.cursor/mcp.json` | `<project_folder>\.cursor\mcp.json` | `mcpServers` |
|
||||
| **Windsurf** | Global | `~/.codeium/windsurf/mcp_config.json` | `%USERPROFILE%\.codeium\windsurf\mcp_config.json` | `mcpServers` |
|
||||
| **VS Code** | Project | `<project_folder>/.vscode/mcp.json` | `<project_folder>\.vscode\mcp.json` | `servers` |
|
||||
| **Q CLI** | Global | `~/.aws/amazonq/mcp.json` | | `mcpServers` |
|
||||
|
||||
##### Manual Configuration
|
||||
|
||||
###### Cursor & Windsurf & Q Developer CLI (`mcpServers`)
|
||||
###### Cursor & Windsurf (`mcpServers`)
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"task-master-ai": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "task-master-ai"],
|
||||
"args": ["-y", "--package=task-master-ai", "task-master-ai"],
|
||||
"env": {
|
||||
"ANTHROPIC_API_KEY": "YOUR_ANTHROPIC_API_KEY_HERE",
|
||||
"PERPLEXITY_API_KEY": "YOUR_PERPLEXITY_API_KEY_HERE",
|
||||
@@ -136,7 +122,7 @@ MCP (Model Control Protocol) lets you run Task Master directly from your editor.
|
||||
|
||||
> 🔑 Replace `YOUR_…_KEY_HERE` with your real API keys. You can remove keys you don't use.
|
||||
|
||||
> **Note**: If you see `0 tools enabled` in the MCP settings, restart your editor and check that your API keys are correctly configured.
|
||||
> **Note**: If you see `0 tools enabled` in the MCP settings, try removing the `--package=task-master-ai` flag from `args`.
|
||||
|
||||
###### VS Code (`servers` + `type`)
|
||||
|
||||
@@ -145,7 +131,7 @@ MCP (Model Control Protocol) lets you run Task Master directly from your editor.
|
||||
"servers": {
|
||||
"task-master-ai": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "task-master-ai"],
|
||||
"args": ["-y", "--package=task-master-ai", "task-master-ai"],
|
||||
"env": {
|
||||
"ANTHROPIC_API_KEY": "YOUR_ANTHROPIC_API_KEY_HERE",
|
||||
"PERPLEXITY_API_KEY": "YOUR_PERPLEXITY_API_KEY_HERE",
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
# @tm/cli
|
||||
|
||||
## null
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tm/core@null
|
||||
|
||||
## 0.27.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tm/core@0.26.1
|
||||
|
||||
## 0.27.0-rc.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#1213](https://github.com/eyaltoledano/claude-task-master/pull/1213) [`137ef36`](https://github.com/eyaltoledano/claude-task-master/commit/137ef362789a9cdfdb1925e35e0438c1fa6c69ee) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - testing this stuff out to see how the release candidate works with monorepo
|
||||
|
||||
## 1.1.0-rc.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#1213](https://github.com/eyaltoledano/claude-task-master/pull/1213) [`cd90b4d`](https://github.com/eyaltoledano/claude-task-master/commit/cd90b4d65fc2f04bdad9fb73aba320b58a124240) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - testing this stuff out to see how the release candidate works with monorepo
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"name": "@tm/cli",
|
||||
"version": "1.0.0",
|
||||
"description": "Task Master CLI - Command line interface for task management",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
@@ -23,19 +24,19 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@tm/core": "*",
|
||||
"boxen": "^8.0.1",
|
||||
"boxen": "^7.1.1",
|
||||
"chalk": "5.6.2",
|
||||
"cli-table3": "^0.6.5",
|
||||
"commander": "^12.1.0",
|
||||
"inquirer": "^12.5.0",
|
||||
"ora": "^8.2.0"
|
||||
"inquirer": "^9.2.10",
|
||||
"ora": "^8.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@types/inquirer": "^9.0.3",
|
||||
"@types/node": "^22.10.5",
|
||||
"tsx": "^4.20.4",
|
||||
"typescript": "^5.9.2",
|
||||
"typescript": "^5.7.3",
|
||||
"vitest": "^2.1.8"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import inquirer from 'inquirer';
|
||||
import ora, { Ora } from 'ora';
|
||||
import ora from 'ora';
|
||||
import {
|
||||
AuthManager,
|
||||
AuthenticationError,
|
||||
@@ -49,15 +49,8 @@ export class ContextCommand extends Command {
|
||||
this.addClearCommand();
|
||||
this.addSetCommand();
|
||||
|
||||
// Accept optional positional argument for brief ID or Hamster URL
|
||||
this.argument('[briefOrUrl]', 'Brief ID or Hamster brief URL');
|
||||
|
||||
// Default action: if an argument is provided, resolve and set context; else show
|
||||
this.action(async (briefOrUrl?: string) => {
|
||||
if (briefOrUrl && briefOrUrl.trim().length > 0) {
|
||||
await this.executeSetFromBriefInput(briefOrUrl.trim());
|
||||
return;
|
||||
}
|
||||
// Default action shows current context
|
||||
this.action(async () => {
|
||||
await this.executeShow();
|
||||
});
|
||||
}
|
||||
@@ -333,7 +326,7 @@ export class ContextCommand extends Command {
|
||||
choices: [
|
||||
{ name: '(No brief - organization level)', value: null },
|
||||
...briefs.map((brief) => ({
|
||||
name: `Brief ${brief.id} (${new Date(brief.createdAt).toLocaleDateString()})`,
|
||||
name: `Brief ${brief.id.slice(0, 8)} (${new Date(brief.createdAt).toLocaleDateString()})`,
|
||||
value: brief
|
||||
}))
|
||||
]
|
||||
@@ -448,142 +441,6 @@ export class ContextCommand extends Command {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute setting context from a brief ID or Hamster URL
|
||||
*/
|
||||
private async executeSetFromBriefInput(briefOrUrl: string): Promise<void> {
|
||||
let spinner: Ora | undefined;
|
||||
try {
|
||||
// Check authentication
|
||||
if (!this.authManager.isAuthenticated()) {
|
||||
ui.displayError('Not authenticated. Run "tm auth login" first.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
spinner = ora('Resolving brief...');
|
||||
spinner.start();
|
||||
|
||||
// Extract brief ID
|
||||
const briefId = this.extractBriefId(briefOrUrl);
|
||||
if (!briefId) {
|
||||
spinner.fail('Could not extract a brief ID from the provided input');
|
||||
ui.displayError(
|
||||
`Provide a valid brief ID or a Hamster brief URL, e.g. https://${process.env.TM_PUBLIC_BASE_DOMAIN}/home/hamster/briefs/<id>`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Fetch brief and resolve its organization
|
||||
const brief = await this.authManager.getBrief(briefId);
|
||||
if (!brief) {
|
||||
spinner.fail('Brief not found or you do not have access');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Fetch org to get a friendly name (optional)
|
||||
let orgName: string | undefined;
|
||||
try {
|
||||
const org = await this.authManager.getOrganization(brief.accountId);
|
||||
orgName = org?.name;
|
||||
} catch {
|
||||
// Non-fatal if org lookup fails
|
||||
}
|
||||
|
||||
// Update context: set org and brief
|
||||
const briefName = `Brief ${brief.id.slice(0, 8)}`;
|
||||
await this.authManager.updateContext({
|
||||
orgId: brief.accountId,
|
||||
orgName,
|
||||
briefId: brief.id,
|
||||
briefName
|
||||
});
|
||||
|
||||
spinner.succeed('Context set from brief');
|
||||
console.log(
|
||||
chalk.gray(
|
||||
` Organization: ${orgName || brief.accountId}\n Brief: ${briefName}`
|
||||
)
|
||||
);
|
||||
|
||||
this.setLastResult({
|
||||
success: true,
|
||||
action: 'set',
|
||||
context: this.authManager.getContext() || undefined,
|
||||
message: 'Context set from brief'
|
||||
});
|
||||
} catch (error: any) {
|
||||
try {
|
||||
if (spinner?.isSpinning) spinner.stop();
|
||||
} catch {}
|
||||
this.handleError(error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a brief ID from raw input (ID or Hamster URL)
|
||||
*/
|
||||
private extractBriefId(input: string): string | null {
|
||||
const raw = input?.trim() ?? '';
|
||||
if (!raw) return null;
|
||||
|
||||
const parseUrl = (s: string): URL | null => {
|
||||
try {
|
||||
return new URL(s);
|
||||
} catch {}
|
||||
try {
|
||||
return new URL(`https://${s}`);
|
||||
} catch {}
|
||||
return null;
|
||||
};
|
||||
|
||||
const fromParts = (path: string): string | null => {
|
||||
const parts = path.split('/').filter(Boolean);
|
||||
const briefsIdx = parts.lastIndexOf('briefs');
|
||||
const candidate =
|
||||
briefsIdx >= 0 && parts.length > briefsIdx + 1
|
||||
? parts[briefsIdx + 1]
|
||||
: parts[parts.length - 1];
|
||||
return candidate?.trim() || null;
|
||||
};
|
||||
|
||||
// 1) URL (absolute or scheme‑less)
|
||||
const url = parseUrl(raw);
|
||||
if (url) {
|
||||
const qId = url.searchParams.get('id') || url.searchParams.get('briefId');
|
||||
const candidate = (qId || fromParts(url.pathname)) ?? null;
|
||||
if (candidate) {
|
||||
// Light sanity check; let API be the final validator
|
||||
if (this.isLikelyId(candidate) || candidate.length >= 8)
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Looks like a path without scheme
|
||||
if (raw.includes('/')) {
|
||||
const candidate = fromParts(raw);
|
||||
if (candidate && (this.isLikelyId(candidate) || candidate.length >= 8)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Fallback: raw token
|
||||
return raw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Heuristic to check if a string looks like a brief ID (UUID-like)
|
||||
*/
|
||||
private isLikelyId(value: string): boolean {
|
||||
const uuidRegex =
|
||||
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
||||
const ulidRegex = /^[0-9A-HJKMNP-TV-Z]{26}$/i; // ULID
|
||||
const slugRegex = /^[A-Za-z0-9_-]{16,}$/; // general token
|
||||
return (
|
||||
uuidRegex.test(value) || ulidRegex.test(value) || slugRegex.test(value)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set context directly from options
|
||||
*/
|
||||
|
||||
@@ -1,318 +0,0 @@
|
||||
/**
|
||||
* @fileoverview SetStatusCommand using Commander's native class pattern
|
||||
* Extends Commander.Command for better integration with the framework
|
||||
*/
|
||||
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import boxen from 'boxen';
|
||||
import {
|
||||
createTaskMasterCore,
|
||||
type TaskMasterCore,
|
||||
type TaskStatus
|
||||
} from '@tm/core';
|
||||
import type { StorageType } from '@tm/core/types';
|
||||
|
||||
/**
|
||||
* Valid task status values for validation
|
||||
*/
|
||||
const VALID_TASK_STATUSES: TaskStatus[] = [
|
||||
'pending',
|
||||
'in-progress',
|
||||
'done',
|
||||
'deferred',
|
||||
'cancelled',
|
||||
'blocked',
|
||||
'review'
|
||||
];
|
||||
|
||||
/**
|
||||
* Options interface for the set-status command
|
||||
*/
|
||||
export interface SetStatusCommandOptions {
|
||||
id?: string;
|
||||
status?: TaskStatus;
|
||||
format?: 'text' | 'json';
|
||||
silent?: boolean;
|
||||
project?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result type from set-status command
|
||||
*/
|
||||
export interface SetStatusResult {
|
||||
success: boolean;
|
||||
updatedTasks: Array<{
|
||||
taskId: string;
|
||||
oldStatus: TaskStatus;
|
||||
newStatus: TaskStatus;
|
||||
}>;
|
||||
storageType: Exclude<StorageType, 'auto'>;
|
||||
}
|
||||
|
||||
/**
|
||||
* SetStatusCommand extending Commander's Command class
|
||||
* This is a thin presentation layer over @tm/core
|
||||
*/
|
||||
export class SetStatusCommand extends Command {
|
||||
private tmCore?: TaskMasterCore;
|
||||
private lastResult?: SetStatusResult;
|
||||
|
||||
constructor(name?: string) {
|
||||
super(name || 'set-status');
|
||||
|
||||
// Configure the command
|
||||
this.description('Update the status of one or more tasks')
|
||||
.requiredOption(
|
||||
'-i, --id <id>',
|
||||
'Task ID(s) to update (comma-separated for multiple, supports subtasks like 5.2)'
|
||||
)
|
||||
.requiredOption(
|
||||
'-s, --status <status>',
|
||||
`New status (${VALID_TASK_STATUSES.join(', ')})`
|
||||
)
|
||||
.option('-f, --format <format>', 'Output format (text, json)', 'text')
|
||||
.option('--silent', 'Suppress output (useful for programmatic usage)')
|
||||
.option('-p, --project <path>', 'Project root directory', process.cwd())
|
||||
.action(async (options: SetStatusCommandOptions) => {
|
||||
await this.executeCommand(options);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the set-status command
|
||||
*/
|
||||
private async executeCommand(
|
||||
options: SetStatusCommandOptions
|
||||
): Promise<void> {
|
||||
try {
|
||||
// Validate required options
|
||||
if (!options.id) {
|
||||
console.error(chalk.red('Error: Task ID is required. Use -i or --id'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!options.status) {
|
||||
console.error(
|
||||
chalk.red('Error: Status is required. Use -s or --status')
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Validate status
|
||||
if (!VALID_TASK_STATUSES.includes(options.status)) {
|
||||
console.error(
|
||||
chalk.red(
|
||||
`Error: Invalid status "${options.status}". Valid options: ${VALID_TASK_STATUSES.join(', ')}`
|
||||
)
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Initialize TaskMaster core
|
||||
this.tmCore = await createTaskMasterCore({
|
||||
projectPath: options.project || process.cwd()
|
||||
});
|
||||
|
||||
// Parse task IDs (handle comma-separated values)
|
||||
const taskIds = options.id.split(',').map((id) => id.trim());
|
||||
|
||||
// Update each task
|
||||
const updatedTasks: Array<{
|
||||
taskId: string;
|
||||
oldStatus: TaskStatus;
|
||||
newStatus: TaskStatus;
|
||||
}> = [];
|
||||
|
||||
for (const taskId of taskIds) {
|
||||
try {
|
||||
const result = await this.tmCore.updateTaskStatus(
|
||||
taskId,
|
||||
options.status
|
||||
);
|
||||
updatedTasks.push({
|
||||
taskId: result.taskId,
|
||||
oldStatus: result.oldStatus,
|
||||
newStatus: result.newStatus
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
|
||||
if (!options.silent) {
|
||||
console.error(
|
||||
chalk.red(`Failed to update task ${taskId}: ${errorMessage}`)
|
||||
);
|
||||
}
|
||||
if (options.format === 'json') {
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
taskId,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Store result for potential reuse
|
||||
this.lastResult = {
|
||||
success: true,
|
||||
updatedTasks,
|
||||
storageType: this.tmCore.getStorageType() as Exclude<
|
||||
StorageType,
|
||||
'auto'
|
||||
>
|
||||
};
|
||||
|
||||
// Display results
|
||||
this.displayResults(this.lastResult, options);
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Unknown error occurred';
|
||||
|
||||
if (!options.silent) {
|
||||
console.error(chalk.red(`Error: ${errorMessage}`));
|
||||
}
|
||||
|
||||
if (options.format === 'json') {
|
||||
console.log(JSON.stringify({ success: false, error: errorMessage }));
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
} finally {
|
||||
// Clean up resources
|
||||
if (this.tmCore) {
|
||||
await this.tmCore.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display results based on format
|
||||
*/
|
||||
private displayResults(
|
||||
result: SetStatusResult,
|
||||
options: SetStatusCommandOptions
|
||||
): void {
|
||||
const format = options.format || 'text';
|
||||
|
||||
switch (format) {
|
||||
case 'json':
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
break;
|
||||
|
||||
case 'text':
|
||||
default:
|
||||
if (!options.silent) {
|
||||
this.displayTextResults(result);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display results in text format
|
||||
*/
|
||||
private displayTextResults(result: SetStatusResult): void {
|
||||
if (result.updatedTasks.length === 1) {
|
||||
// Single task update
|
||||
const update = result.updatedTasks[0];
|
||||
console.log(
|
||||
boxen(
|
||||
chalk.white.bold(`✅ Successfully updated task ${update.taskId}`) +
|
||||
'\n\n' +
|
||||
`${chalk.blue('From:')} ${this.getStatusDisplay(update.oldStatus)}\n` +
|
||||
`${chalk.blue('To:')} ${this.getStatusDisplay(update.newStatus)}`,
|
||||
{
|
||||
padding: 1,
|
||||
borderColor: 'green',
|
||||
borderStyle: 'round',
|
||||
margin: { top: 1 }
|
||||
}
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// Multiple task updates
|
||||
console.log(
|
||||
boxen(
|
||||
chalk.white.bold(
|
||||
`✅ Successfully updated ${result.updatedTasks.length} tasks`
|
||||
) +
|
||||
'\n\n' +
|
||||
result.updatedTasks
|
||||
.map(
|
||||
(update) =>
|
||||
`${chalk.cyan(update.taskId)}: ${this.getStatusDisplay(update.oldStatus)} → ${this.getStatusDisplay(update.newStatus)}`
|
||||
)
|
||||
.join('\n'),
|
||||
{
|
||||
padding: 1,
|
||||
borderColor: 'green',
|
||||
borderStyle: 'round',
|
||||
margin: { top: 1 }
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Show storage info
|
||||
console.log(chalk.gray(`\nUsing ${result.storageType} storage`));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get colored status display
|
||||
*/
|
||||
private getStatusDisplay(status: TaskStatus): string {
|
||||
const statusColors: Record<TaskStatus, (text: string) => string> = {
|
||||
pending: chalk.yellow,
|
||||
'in-progress': chalk.blue,
|
||||
done: chalk.green,
|
||||
deferred: chalk.gray,
|
||||
cancelled: chalk.red,
|
||||
blocked: chalk.red,
|
||||
review: chalk.magenta,
|
||||
completed: chalk.green
|
||||
};
|
||||
|
||||
const colorFn = statusColors[status] || chalk.white;
|
||||
return colorFn(status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last command result (useful for testing or chaining)
|
||||
*/
|
||||
getLastResult(): SetStatusResult | undefined {
|
||||
return this.lastResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static method to register this command on an existing program
|
||||
* This is for gradual migration - allows commands.js to use this
|
||||
*/
|
||||
static registerOn(program: Command): Command {
|
||||
const setStatusCommand = new SetStatusCommand();
|
||||
program.addCommand(setStatusCommand);
|
||||
return setStatusCommand;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alternative registration that returns the command for chaining
|
||||
* Can also configure the command name if needed
|
||||
*/
|
||||
static register(program: Command, name?: string): SetStatusCommand {
|
||||
const setStatusCommand = new SetStatusCommand(name);
|
||||
program.addCommand(setStatusCommand);
|
||||
return setStatusCommand;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory function to create and configure the set-status command
|
||||
*/
|
||||
export function createSetStatusCommand(): SetStatusCommand {
|
||||
return new SetStatusCommand();
|
||||
}
|
||||
@@ -9,7 +9,14 @@ import boxen from 'boxen';
|
||||
import { createTaskMasterCore, type Task, type TaskMasterCore } from '@tm/core';
|
||||
import type { StorageType } from '@tm/core/types';
|
||||
import * as ui from '../utils/ui.js';
|
||||
import { displayTaskDetails } from '../ui/components/task-detail.component.js';
|
||||
import {
|
||||
displayTaskHeader,
|
||||
displayTaskProperties,
|
||||
displayImplementationDetails,
|
||||
displayTestStrategy,
|
||||
displaySubtasks,
|
||||
displaySuggestedActions
|
||||
} from '../ui/components/task-detail.component.js';
|
||||
|
||||
/**
|
||||
* Options interface for the show command
|
||||
@@ -257,11 +264,44 @@ export class ShowCommand extends Command {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the global task details display function
|
||||
displayTaskDetails(result.task, {
|
||||
statusFilter: options.status,
|
||||
showSuggestedActions: true
|
||||
});
|
||||
const task = result.task;
|
||||
|
||||
// Display header with tag
|
||||
displayTaskHeader(task.id, task.title);
|
||||
|
||||
// Display task properties in table format
|
||||
displayTaskProperties(task);
|
||||
|
||||
// Display implementation details if available
|
||||
if (task.details) {
|
||||
console.log(); // Empty line for spacing
|
||||
displayImplementationDetails(task.details);
|
||||
}
|
||||
|
||||
// Display test strategy if available
|
||||
if ('testStrategy' in task && task.testStrategy) {
|
||||
console.log(); // Empty line for spacing
|
||||
displayTestStrategy(task.testStrategy as string);
|
||||
}
|
||||
|
||||
// Display subtasks if available
|
||||
if (task.subtasks && task.subtasks.length > 0) {
|
||||
// Filter subtasks by status if provided
|
||||
const filteredSubtasks = options.status
|
||||
? task.subtasks.filter((sub) => sub.status === options.status)
|
||||
: task.subtasks;
|
||||
|
||||
if (filteredSubtasks.length === 0 && options.status) {
|
||||
console.log(
|
||||
chalk.gray(` No subtasks with status '${options.status}'`)
|
||||
);
|
||||
} else {
|
||||
displaySubtasks(filteredSubtasks, task.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Display suggested actions
|
||||
displaySuggestedActions(task.id);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,512 +0,0 @@
|
||||
/**
|
||||
* @fileoverview StartCommand using Commander's native class pattern
|
||||
* Extends Commander.Command for better integration with the framework
|
||||
* This is a thin presentation layer over @tm/core's TaskExecutionService
|
||||
*/
|
||||
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import boxen from 'boxen';
|
||||
import ora, { type Ora } from 'ora';
|
||||
import { spawn } from 'child_process';
|
||||
import {
|
||||
createTaskMasterCore,
|
||||
type TaskMasterCore,
|
||||
type StartTaskResult as CoreStartTaskResult
|
||||
} from '@tm/core';
|
||||
import { displayTaskDetails } from '../ui/components/task-detail.component.js';
|
||||
import * as ui from '../utils/ui.js';
|
||||
|
||||
/**
|
||||
* CLI-specific options interface for the start command
|
||||
*/
|
||||
export interface StartCommandOptions {
|
||||
id?: string;
|
||||
format?: 'text' | 'json';
|
||||
project?: string;
|
||||
dryRun?: boolean;
|
||||
force?: boolean;
|
||||
noStatusUpdate?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* CLI-specific result type from start command
|
||||
* Extends the core result with CLI-specific display information
|
||||
*/
|
||||
export interface StartCommandResult extends CoreStartTaskResult {
|
||||
storageType?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* StartCommand extending Commander's Command class
|
||||
* This is a thin presentation layer over @tm/core's TaskExecutionService
|
||||
*/
|
||||
export class StartCommand extends Command {
|
||||
private tmCore?: TaskMasterCore;
|
||||
private lastResult?: StartCommandResult;
|
||||
|
||||
constructor(name?: string) {
|
||||
super(name || 'start');
|
||||
|
||||
// Configure the command
|
||||
this.description(
|
||||
'Start working on a task by launching claude-code with context'
|
||||
)
|
||||
.argument('[id]', 'Task ID to start working on')
|
||||
.option('-i, --id <id>', 'Task ID to start working on')
|
||||
.option('-f, --format <format>', 'Output format (text, json)', 'text')
|
||||
.option('-p, --project <path>', 'Project root directory', process.cwd())
|
||||
.option(
|
||||
'--dry-run',
|
||||
'Show what would be executed without launching claude-code'
|
||||
)
|
||||
.option(
|
||||
'--force',
|
||||
'Force start even if another task is already in-progress'
|
||||
)
|
||||
.option(
|
||||
'--no-status-update',
|
||||
'Do not automatically update task status to in-progress'
|
||||
)
|
||||
.action(
|
||||
async (taskId: string | undefined, options: StartCommandOptions) => {
|
||||
await this.executeCommand(taskId, options);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the start command
|
||||
*/
|
||||
private async executeCommand(
|
||||
taskId: string | undefined,
|
||||
options: StartCommandOptions
|
||||
): Promise<void> {
|
||||
let spinner: Ora | null = null;
|
||||
|
||||
try {
|
||||
// Validate options
|
||||
if (!this.validateOptions(options)) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Initialize tm-core with spinner
|
||||
spinner = ora('Initializing Task Master...').start();
|
||||
await this.initializeCore(options.project || process.cwd());
|
||||
spinner.succeed('Task Master initialized');
|
||||
|
||||
// Get the task ID from argument or option, or find next available task
|
||||
const idArg = taskId || options.id || null;
|
||||
let targetTaskId = idArg;
|
||||
|
||||
if (!targetTaskId) {
|
||||
spinner = ora('Finding next available task...').start();
|
||||
targetTaskId = await this.performGetNextTask();
|
||||
if (targetTaskId) {
|
||||
spinner.succeed(`Found next task: #${targetTaskId}`);
|
||||
} else {
|
||||
spinner.fail('No available tasks found');
|
||||
}
|
||||
}
|
||||
|
||||
if (!targetTaskId) {
|
||||
ui.displayError('No task ID provided and no available tasks found');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Show pre-launch message (no spinner needed, it's just display)
|
||||
if (!options.dryRun) {
|
||||
await this.showPreLaunchMessage(targetTaskId);
|
||||
}
|
||||
|
||||
// Use tm-core's startTask method with spinner
|
||||
spinner = ora('Preparing task execution...').start();
|
||||
const coreResult = await this.performStartTask(targetTaskId, options);
|
||||
|
||||
if (coreResult.started) {
|
||||
spinner.succeed(
|
||||
options.dryRun
|
||||
? 'Dry run completed'
|
||||
: 'Task prepared - launching Claude...'
|
||||
);
|
||||
} else {
|
||||
spinner.fail('Task execution failed');
|
||||
}
|
||||
|
||||
// Execute command if we have one and it's not a dry run
|
||||
if (!options.dryRun && coreResult.command) {
|
||||
// Stop any remaining spinners before launching Claude
|
||||
if (spinner && !spinner.isSpinning) {
|
||||
// Clear the line to make room for Claude
|
||||
console.log();
|
||||
}
|
||||
await this.executeChildProcess(coreResult.command);
|
||||
}
|
||||
|
||||
// Convert core result to CLI result with storage type
|
||||
const result: StartCommandResult = {
|
||||
...coreResult,
|
||||
storageType: this.tmCore?.getStorageType()
|
||||
};
|
||||
|
||||
// Store result for programmatic access
|
||||
this.setLastResult(result);
|
||||
|
||||
// Display results (only for dry run or if execution failed)
|
||||
if (options.dryRun || !coreResult.started) {
|
||||
this.displayResults(result, options);
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (spinner) {
|
||||
spinner.fail('Operation failed');
|
||||
}
|
||||
this.handleError(error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate command options
|
||||
*/
|
||||
private validateOptions(options: StartCommandOptions): boolean {
|
||||
// Validate format
|
||||
if (options.format && !['text', 'json'].includes(options.format)) {
|
||||
console.error(chalk.red(`Invalid format: ${options.format}`));
|
||||
console.error(chalk.gray(`Valid formats: text, json`));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize TaskMasterCore
|
||||
*/
|
||||
private async initializeCore(projectRoot: string): Promise<void> {
|
||||
if (!this.tmCore) {
|
||||
this.tmCore = await createTaskMasterCore({ projectPath: projectRoot });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next available task using tm-core
|
||||
*/
|
||||
private async performGetNextTask(): Promise<string | null> {
|
||||
if (!this.tmCore) {
|
||||
throw new Error('TaskMasterCore not initialized');
|
||||
}
|
||||
return this.tmCore.getNextAvailableTask();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show pre-launch message using tm-core data
|
||||
*/
|
||||
private async showPreLaunchMessage(targetTaskId: string): Promise<void> {
|
||||
if (!this.tmCore) return;
|
||||
|
||||
const { task, subtask, subtaskId } =
|
||||
await this.tmCore.getTaskWithSubtask(targetTaskId);
|
||||
if (task) {
|
||||
const workItemText = subtask
|
||||
? `Subtask #${task.id}.${subtaskId} - ${subtask.title}`
|
||||
: `Task #${task.id} - ${task.title}`;
|
||||
|
||||
console.log(
|
||||
chalk.green('🚀 Starting: ') + chalk.white.bold(workItemText)
|
||||
);
|
||||
console.log(chalk.gray('Launching Claude Code...'));
|
||||
console.log(); // Empty line
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform start task using tm-core business logic
|
||||
*/
|
||||
private async performStartTask(
|
||||
targetTaskId: string,
|
||||
options: StartCommandOptions
|
||||
): Promise<CoreStartTaskResult> {
|
||||
if (!this.tmCore) {
|
||||
throw new Error('TaskMasterCore not initialized');
|
||||
}
|
||||
|
||||
// Show spinner for status update if enabled
|
||||
let statusSpinner: Ora | null = null;
|
||||
if (!options.noStatusUpdate && !options.dryRun) {
|
||||
statusSpinner = ora('Updating task status to in-progress...').start();
|
||||
}
|
||||
|
||||
// Get execution command from tm-core (instead of executing directly)
|
||||
const result = await this.tmCore.startTask(targetTaskId, {
|
||||
dryRun: options.dryRun,
|
||||
force: options.force,
|
||||
updateStatus: !options.noStatusUpdate
|
||||
});
|
||||
|
||||
if (statusSpinner) {
|
||||
if (result.started) {
|
||||
statusSpinner.succeed('Task status updated');
|
||||
} else {
|
||||
statusSpinner.warn('Task status update skipped');
|
||||
}
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
throw new Error('Failed to start task - core result is undefined');
|
||||
}
|
||||
|
||||
// Don't execute here - let the main executeCommand method handle it
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the child process directly in the main thread for better process control
|
||||
*/
|
||||
private async executeChildProcess(command: {
|
||||
executable: string;
|
||||
args: string[];
|
||||
cwd: string;
|
||||
}): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Don't show the full command with args as it can be very long
|
||||
console.log(chalk.green('🚀 Launching Claude Code...'));
|
||||
console.log(); // Add space before Claude takes over
|
||||
|
||||
const childProcess = spawn(command.executable, command.args, {
|
||||
cwd: command.cwd,
|
||||
stdio: 'inherit', // Inherit stdio from parent process
|
||||
shell: false
|
||||
});
|
||||
|
||||
childProcess.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`Process exited with code ${code}`));
|
||||
}
|
||||
});
|
||||
|
||||
childProcess.on('error', (error) => {
|
||||
reject(new Error(`Failed to spawn process: ${error.message}`));
|
||||
});
|
||||
|
||||
// Handle process termination signals gracefully
|
||||
const cleanup = () => {
|
||||
if (childProcess && !childProcess.killed) {
|
||||
childProcess.kill('SIGTERM');
|
||||
}
|
||||
};
|
||||
|
||||
process.on('SIGINT', cleanup);
|
||||
process.on('SIGTERM', cleanup);
|
||||
process.on('exit', cleanup);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Display results based on format
|
||||
*/
|
||||
private displayResults(
|
||||
result: StartCommandResult,
|
||||
options: StartCommandOptions
|
||||
): void {
|
||||
const format = options.format || 'text';
|
||||
|
||||
switch (format) {
|
||||
case 'json':
|
||||
this.displayJson(result);
|
||||
break;
|
||||
|
||||
case 'text':
|
||||
default:
|
||||
this.displayTextResult(result, options);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display in JSON format
|
||||
*/
|
||||
private displayJson(result: StartCommandResult): void {
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display result in text format
|
||||
*/
|
||||
private displayTextResult(
|
||||
result: StartCommandResult,
|
||||
options: StartCommandOptions
|
||||
): void {
|
||||
if (!result.found || !result.task) {
|
||||
console.log(
|
||||
boxen(chalk.yellow(`Task not found!`), {
|
||||
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
||||
borderColor: 'yellow',
|
||||
borderStyle: 'round',
|
||||
margin: { top: 1 }
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const task = result.task;
|
||||
|
||||
if (options.dryRun) {
|
||||
// For dry run, show full details since Claude Code won't be launched
|
||||
let headerText = `Dry Run: Starting Task #${task.id} - ${task.title}`;
|
||||
|
||||
// If working on a specific subtask, highlight it in the header
|
||||
if (result.subtask && result.subtaskId) {
|
||||
headerText = `Dry Run: Starting Subtask #${task.id}.${result.subtaskId} - ${result.subtask.title}`;
|
||||
}
|
||||
|
||||
displayTaskDetails(task, {
|
||||
customHeader: headerText,
|
||||
headerColor: 'yellow'
|
||||
});
|
||||
|
||||
// Show claude-code prompt
|
||||
if (result.executionOutput) {
|
||||
console.log(); // Empty line for spacing
|
||||
console.log(
|
||||
boxen(
|
||||
chalk.white.bold('Claude-Code Prompt:') +
|
||||
'\n\n' +
|
||||
result.executionOutput,
|
||||
{
|
||||
padding: 1,
|
||||
borderStyle: 'round',
|
||||
borderColor: 'cyan',
|
||||
width: process.stdout.columns * 0.95 || 100
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
console.log(); // Empty line for spacing
|
||||
console.log(
|
||||
boxen(
|
||||
chalk.yellow(
|
||||
'🔍 Dry run - claude-code would be launched with the above prompt'
|
||||
),
|
||||
{
|
||||
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
||||
borderColor: 'yellow',
|
||||
borderStyle: 'round'
|
||||
}
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// For actual execution, show minimal info since Claude Code will clear the terminal
|
||||
if (result.started) {
|
||||
// Determine what was worked on - task or subtask
|
||||
let workItemText = `Task: #${task.id} - ${task.title}`;
|
||||
let statusTarget = task.id;
|
||||
|
||||
if (result.subtask && result.subtaskId) {
|
||||
workItemText = `Subtask: #${task.id}.${result.subtaskId} - ${result.subtask.title}`;
|
||||
statusTarget = `${task.id}.${result.subtaskId}`;
|
||||
}
|
||||
|
||||
// Post-execution message (shown after Claude Code exits)
|
||||
console.log(
|
||||
boxen(
|
||||
chalk.green.bold('🎉 Task Session Complete!') +
|
||||
'\n\n' +
|
||||
chalk.white(workItemText) +
|
||||
'\n\n' +
|
||||
chalk.cyan('Next steps:') +
|
||||
'\n' +
|
||||
`• Run ${chalk.yellow('tm show ' + task.id)} to review task details\n` +
|
||||
`• Run ${chalk.yellow('tm set-status --id=' + statusTarget + ' --status=done')} when complete\n` +
|
||||
`• Run ${chalk.yellow('tm next')} to find the next available task\n` +
|
||||
`• Run ${chalk.yellow('tm start')} to begin the next task`,
|
||||
{
|
||||
padding: 1,
|
||||
borderStyle: 'round',
|
||||
borderColor: 'green',
|
||||
width: process.stdout.columns * 0.95 || 100,
|
||||
margin: { top: 1 }
|
||||
}
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// Error case
|
||||
console.log(
|
||||
boxen(
|
||||
chalk.red(
|
||||
'❌ Failed to launch claude-code' +
|
||||
(result.error ? `\nError: ${result.error}` : '')
|
||||
),
|
||||
{
|
||||
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
||||
borderColor: 'red',
|
||||
borderStyle: 'round'
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n${chalk.gray('Storage: ' + result.storageType)}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle general errors
|
||||
*/
|
||||
private handleError(error: any): void {
|
||||
const msg = error?.getSanitizedDetails?.() ?? {
|
||||
message: error?.message ?? String(error)
|
||||
};
|
||||
console.error(chalk.red(`Error: ${msg.message || 'Unexpected error'}`));
|
||||
|
||||
// Show stack trace in development mode or when DEBUG is set
|
||||
const isDevelopment = process.env.NODE_ENV !== 'production';
|
||||
if ((isDevelopment || process.env.DEBUG) && error.stack) {
|
||||
console.error(chalk.gray(error.stack));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the last result for programmatic access
|
||||
*/
|
||||
private setLastResult(result: StartCommandResult): void {
|
||||
this.lastResult = result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last result (for programmatic usage)
|
||||
*/
|
||||
getLastResult(): StartCommandResult | undefined {
|
||||
return this.lastResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up resources
|
||||
*/
|
||||
async cleanup(): Promise<void> {
|
||||
if (this.tmCore) {
|
||||
await this.tmCore.close();
|
||||
this.tmCore = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Static method to register this command on an existing program
|
||||
*/
|
||||
static registerOn(program: Command): Command {
|
||||
const startCommand = new StartCommand();
|
||||
program.addCommand(startCommand);
|
||||
return startCommand;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alternative registration that returns the command for chaining
|
||||
*/
|
||||
static register(program: Command, name?: string): StartCommand {
|
||||
const startCommand = new StartCommand(name);
|
||||
program.addCommand(startCommand);
|
||||
return startCommand;
|
||||
}
|
||||
}
|
||||
@@ -8,20 +8,10 @@ export { ListTasksCommand } from './commands/list.command.js';
|
||||
export { ShowCommand } from './commands/show.command.js';
|
||||
export { AuthCommand } from './commands/auth.command.js';
|
||||
export { ContextCommand } from './commands/context.command.js';
|
||||
export { StartCommand } from './commands/start.command.js';
|
||||
export { SetStatusCommand } from './commands/set-status.command.js';
|
||||
|
||||
// UI utilities (for other commands to use)
|
||||
export * as ui from './utils/ui.js';
|
||||
|
||||
// Auto-update utilities
|
||||
export {
|
||||
checkForUpdate,
|
||||
performAutoUpdate,
|
||||
displayUpgradeNotification,
|
||||
compareVersions
|
||||
} from './utils/auto-update.js';
|
||||
|
||||
// Re-export commonly used types from tm-core
|
||||
export type {
|
||||
Task,
|
||||
|
||||
@@ -262,74 +262,3 @@ export function displaySuggestedActions(taskId: string | number): void {
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display complete task details - used by both show and start commands
|
||||
*/
|
||||
export function displayTaskDetails(
|
||||
task: Task,
|
||||
options?: {
|
||||
statusFilter?: string;
|
||||
showSuggestedActions?: boolean;
|
||||
customHeader?: string;
|
||||
headerColor?: string;
|
||||
}
|
||||
): void {
|
||||
const {
|
||||
statusFilter,
|
||||
showSuggestedActions = false,
|
||||
customHeader,
|
||||
headerColor = 'blue'
|
||||
} = options || {};
|
||||
|
||||
// Display header - either custom or default
|
||||
if (customHeader) {
|
||||
console.log(
|
||||
boxen(chalk.white.bold(customHeader), {
|
||||
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
||||
borderColor: headerColor,
|
||||
borderStyle: 'round',
|
||||
margin: { top: 1 }
|
||||
})
|
||||
);
|
||||
} else {
|
||||
displayTaskHeader(task.id, task.title);
|
||||
}
|
||||
|
||||
// Display task properties in table format
|
||||
displayTaskProperties(task);
|
||||
|
||||
// Display implementation details if available
|
||||
if (task.details) {
|
||||
console.log(); // Empty line for spacing
|
||||
displayImplementationDetails(task.details);
|
||||
}
|
||||
|
||||
// Display test strategy if available
|
||||
if ('testStrategy' in task && task.testStrategy) {
|
||||
console.log(); // Empty line for spacing
|
||||
displayTestStrategy(task.testStrategy as string);
|
||||
}
|
||||
|
||||
// Display subtasks if available
|
||||
if (task.subtasks && task.subtasks.length > 0) {
|
||||
// Filter subtasks by status if provided
|
||||
const filteredSubtasks = statusFilter
|
||||
? task.subtasks.filter((sub) => sub.status === statusFilter)
|
||||
: task.subtasks;
|
||||
|
||||
if (filteredSubtasks.length === 0 && statusFilter) {
|
||||
console.log(); // Empty line for spacing
|
||||
console.log(chalk.gray(` No subtasks with status '${statusFilter}'`));
|
||||
} else if (filteredSubtasks.length > 0) {
|
||||
console.log(); // Empty line for spacing
|
||||
displaySubtasks(filteredSubtasks, task.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Display suggested actions if requested
|
||||
if (showSuggestedActions) {
|
||||
console.log(); // Empty line for spacing
|
||||
displaySuggestedActions(task.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,240 +0,0 @@
|
||||
/**
|
||||
* @fileoverview Auto-update utilities for task-master-ai CLI
|
||||
*/
|
||||
|
||||
import { spawn } from 'child_process';
|
||||
import https from 'https';
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
import boxen from 'boxen';
|
||||
|
||||
export interface UpdateInfo {
|
||||
currentVersion: string;
|
||||
latestVersion: string;
|
||||
needsUpdate: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current version from build-time injected environment variable
|
||||
*/
|
||||
function getCurrentVersion(): string {
|
||||
// Version is injected at build time via TM_PUBLIC_VERSION
|
||||
const version = process.env.TM_PUBLIC_VERSION;
|
||||
if (version && version !== 'unknown') {
|
||||
return version;
|
||||
}
|
||||
|
||||
// Fallback for development or if injection failed
|
||||
console.warn('Could not read version from TM_PUBLIC_VERSION, using fallback');
|
||||
return '0.0.0';
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare semantic versions with proper pre-release handling
|
||||
* @param v1 - First version
|
||||
* @param v2 - Second version
|
||||
* @returns -1 if v1 < v2, 0 if v1 = v2, 1 if v1 > v2
|
||||
*/
|
||||
export function compareVersions(v1: string, v2: string): number {
|
||||
const toParts = (v: string) => {
|
||||
const [core, pre = ''] = v.split('-', 2);
|
||||
const nums = core.split('.').map((n) => Number.parseInt(n, 10) || 0);
|
||||
return { nums, pre };
|
||||
};
|
||||
|
||||
const a = toParts(v1);
|
||||
const b = toParts(v2);
|
||||
const len = Math.max(a.nums.length, b.nums.length);
|
||||
|
||||
// Compare numeric parts
|
||||
for (let i = 0; i < len; i++) {
|
||||
const d = (a.nums[i] || 0) - (b.nums[i] || 0);
|
||||
if (d !== 0) return d < 0 ? -1 : 1;
|
||||
}
|
||||
|
||||
// Handle pre-release comparison
|
||||
if (a.pre && !b.pre) return -1; // prerelease < release
|
||||
if (!a.pre && b.pre) return 1; // release > prerelease
|
||||
if (a.pre === b.pre) return 0; // same or both empty
|
||||
return a.pre < b.pre ? -1 : 1; // basic prerelease tie-break
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for newer version of task-master-ai
|
||||
*/
|
||||
export async function checkForUpdate(
|
||||
currentVersionOverride?: string
|
||||
): Promise<UpdateInfo> {
|
||||
const currentVersion = currentVersionOverride || getCurrentVersion();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const options = {
|
||||
hostname: 'registry.npmjs.org',
|
||||
path: '/task-master-ai',
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/vnd.npm.install-v1+json',
|
||||
'User-Agent': `task-master-ai/${currentVersion}`
|
||||
}
|
||||
};
|
||||
|
||||
const req = https.request(options, (res) => {
|
||||
let data = '';
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
try {
|
||||
if (res.statusCode !== 200)
|
||||
throw new Error(`npm registry status ${res.statusCode}`);
|
||||
const npmData = JSON.parse(data);
|
||||
const latestVersion = npmData['dist-tags']?.latest || currentVersion;
|
||||
|
||||
const needsUpdate =
|
||||
compareVersions(currentVersion, latestVersion) < 0;
|
||||
|
||||
resolve({
|
||||
currentVersion,
|
||||
latestVersion,
|
||||
needsUpdate
|
||||
});
|
||||
} catch (error) {
|
||||
resolve({
|
||||
currentVersion,
|
||||
latestVersion: currentVersion,
|
||||
needsUpdate: false
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', () => {
|
||||
resolve({
|
||||
currentVersion,
|
||||
latestVersion: currentVersion,
|
||||
needsUpdate: false
|
||||
});
|
||||
});
|
||||
|
||||
req.setTimeout(3000, () => {
|
||||
req.destroy();
|
||||
resolve({
|
||||
currentVersion,
|
||||
latestVersion: currentVersion,
|
||||
needsUpdate: false
|
||||
});
|
||||
});
|
||||
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Display upgrade notification message
|
||||
*/
|
||||
export function displayUpgradeNotification(
|
||||
currentVersion: string,
|
||||
latestVersion: string
|
||||
) {
|
||||
const message = boxen(
|
||||
`${chalk.blue.bold('Update Available!')} ${chalk.dim(currentVersion)} → ${chalk.green(latestVersion)}\n\n` +
|
||||
`Auto-updating to the latest version with new features and bug fixes...`,
|
||||
{
|
||||
padding: 1,
|
||||
margin: { top: 1, bottom: 1 },
|
||||
borderColor: 'yellow',
|
||||
borderStyle: 'round'
|
||||
}
|
||||
);
|
||||
|
||||
console.log(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically update task-master-ai to the latest version
|
||||
*/
|
||||
export async function performAutoUpdate(
|
||||
latestVersion: string
|
||||
): Promise<boolean> {
|
||||
if (process.env.TASKMASTER_SKIP_AUTO_UPDATE === '1' || process.env.CI) {
|
||||
console.log(
|
||||
chalk.dim('Skipping auto-update (TASKMASTER_SKIP_AUTO_UPDATE/CI).')
|
||||
);
|
||||
return false;
|
||||
}
|
||||
const spinner = ora({
|
||||
text: chalk.blue(
|
||||
`Updating task-master-ai to version ${chalk.green(latestVersion)}`
|
||||
),
|
||||
spinner: 'dots',
|
||||
color: 'blue'
|
||||
}).start();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const updateProcess = spawn(
|
||||
'npm',
|
||||
[
|
||||
'install',
|
||||
'-g',
|
||||
`task-master-ai@${latestVersion}`,
|
||||
'--no-fund',
|
||||
'--no-audit',
|
||||
'--loglevel=warn'
|
||||
],
|
||||
{
|
||||
stdio: ['ignore', 'pipe', 'pipe']
|
||||
}
|
||||
);
|
||||
|
||||
let errorOutput = '';
|
||||
|
||||
updateProcess.stdout.on('data', () => {
|
||||
// Update spinner text with progress
|
||||
spinner.text = chalk.blue(
|
||||
`Installing task-master-ai@${latestVersion}...`
|
||||
);
|
||||
});
|
||||
|
||||
updateProcess.stderr.on('data', (data) => {
|
||||
errorOutput += data.toString();
|
||||
});
|
||||
|
||||
updateProcess.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
spinner.succeed(
|
||||
chalk.green(
|
||||
`Successfully updated to version ${chalk.bold(latestVersion)}`
|
||||
)
|
||||
);
|
||||
console.log(
|
||||
chalk.dim('Please restart your command to use the new version.')
|
||||
);
|
||||
resolve(true);
|
||||
} else {
|
||||
spinner.fail(chalk.red('Auto-update failed'));
|
||||
console.log(
|
||||
chalk.cyan(
|
||||
`Please run manually: npm install -g task-master-ai@${latestVersion}`
|
||||
)
|
||||
);
|
||||
if (errorOutput) {
|
||||
console.log(chalk.dim(`Error: ${errorOutput.trim()}`));
|
||||
}
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
|
||||
updateProcess.on('error', (error) => {
|
||||
spinner.fail(chalk.red('Auto-update failed'));
|
||||
console.log(chalk.red('Error:'), error.message);
|
||||
console.log(
|
||||
chalk.cyan(
|
||||
`Please run manually: npm install -g task-master-ai@${latestVersion}`
|
||||
)
|
||||
);
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,9 +1,5 @@
|
||||
# docs
|
||||
|
||||
## 0.0.4
|
||||
|
||||
## 0.0.3
|
||||
|
||||
## 0.0.2
|
||||
|
||||
## 0.0.1
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
# Task Master Documentation
|
||||
|
||||
Welcome to the Task Master documentation. This documentation site provides comprehensive guides for getting started with Task Master.
|
||||
Welcome to the Task Master documentation. Use the links below to navigate to the information you need:
|
||||
|
||||
## Getting Started
|
||||
|
||||
- [Quick Start Guide](/getting-started/quick-start) - Complete setup and first-time usage guide
|
||||
- [Requirements](/getting-started/quick-start/requirements) - What you need to get started
|
||||
- [Installation](/getting-started/quick-start/installation) - How to install Task Master
|
||||
- [Configuration Guide](archive/configuration.md) - Set up environment variables and customize Task Master
|
||||
- [Tutorial](archive/ctutorial.md) - Step-by-step guide to getting started with Task Master
|
||||
|
||||
## Core Capabilities
|
||||
## Reference
|
||||
|
||||
- [MCP Tools](/capabilities/mcp) - Model Control Protocol integration
|
||||
- [CLI Commands](/capabilities/cli-root-commands) - Command line interface reference
|
||||
- [Task Structure](/capabilities/task-structure) - Understanding tasks and subtasks
|
||||
- [Command Reference](archive/ccommand-reference.md) - Complete list of all available commands
|
||||
- [Task Structure](archive/ctask-structure.md) - Understanding the task format and features
|
||||
|
||||
## Best Practices
|
||||
## Examples & Licensing
|
||||
|
||||
- [Advanced Configuration](/best-practices/configuration-advanced) - Detailed configuration options
|
||||
- [Advanced Tasks](/best-practices/advanced-tasks) - Working with complex task structures
|
||||
- [Example Interactions](archive/cexamples.md) - Common Cursor AI interaction examples
|
||||
- [Licensing Information](archive/clicensing.md) - Detailed information about the license
|
||||
|
||||
## Need More Help?
|
||||
|
||||
If you can't find what you're looking for in these docs, please check the root README.md or visit our [GitHub repository](https://github.com/eyaltoledano/claude-task-master).
|
||||
If you can't find what you're looking for in these docs, please check the [main README](../README.md) or visit our [GitHub repository](https://github.com/eyaltoledano/claude-task-master).
|
||||
|
||||
@@ -19,7 +19,7 @@ description: "This guide walks you through setting up Task Master in your develo
|
||||
"mcpServers": {
|
||||
"taskmaster-ai": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "task-master-ai"],
|
||||
"args": ["-y", "--package", "task-master-ai", "task-master-mcp"],
|
||||
"env": {
|
||||
"ANTHROPIC_API_KEY": "YOUR_ANTHROPIC_API_KEY_HERE",
|
||||
"PERPLEXITY_API_KEY": "YOUR_PERPLEXITY_API_KEY_HERE",
|
||||
|
||||
@@ -48,7 +48,7 @@ description: "Learn how to set up and use Task Master with Cursor AI"
|
||||
<Step title="Configure with the following details:">
|
||||
- Name: "Task Master"
|
||||
- Type: "Command"
|
||||
- Command: "npx -y task-master-ai"
|
||||
- Command: "npx -y --package task-master-ai task-master-mcp"
|
||||
</Step>
|
||||
<Step title="Save Settings">
|
||||
|
||||
|
||||
@@ -83,8 +83,6 @@ Taskmaster uses two primary methods for configuration:
|
||||
- `VERTEX_PROJECT_ID`: Your Google Cloud project ID for Vertex AI. Required when using the 'vertex' provider.
|
||||
- `VERTEX_LOCATION`: Google Cloud region for Vertex AI (e.g., 'us-central1'). Default is 'us-central1'.
|
||||
- `GOOGLE_APPLICATION_CREDENTIALS`: Path to service account credentials JSON file for Google Cloud auth (alternative to API key for Vertex AI).
|
||||
- **Optional Auto-Update Control:**
|
||||
- `TASKMASTER_SKIP_AUTO_UPDATE`: Set to '1' to disable automatic updates. Also automatically disabled in CI environments (when `CI` environment variable is set).
|
||||
|
||||
**Important:** Settings like model ID selections (`main`, `research`, `fallback`), `maxTokens`, `temperature`, `logLevel`, `defaultSubtasks`, `defaultPriority`, and `projectName` are **managed in `.taskmaster/config.json`** (or `.taskmasterconfig` for unmigrated projects), not environment variables.
|
||||
|
||||
|
||||
@@ -156,7 +156,7 @@ sidebarTitle: "CLI Commands"
|
||||
# Use an alternative tasks file
|
||||
task-master analyze-complexity --file=custom-tasks.json
|
||||
|
||||
# Use your configured research model for research-backed complexity analysis
|
||||
# Use Perplexity AI for research-backed complexity analysis
|
||||
task-master analyze-complexity --research
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
"getting-started/quick-start/execute-quick"
|
||||
]
|
||||
},
|
||||
"getting-started/api-keys",
|
||||
"getting-started/faq",
|
||||
"getting-started/contribute"
|
||||
]
|
||||
|
||||
@@ -1,267 +0,0 @@
|
||||
# API Keys Configuration
|
||||
|
||||
Task Master supports multiple AI providers through environment variables. This page lists all available API keys and their configuration requirements.
|
||||
|
||||
## Required API Keys
|
||||
|
||||
> **Note**: At least one required API key must be configured for Task Master to function.
|
||||
>
|
||||
> "Required: Yes" below means "required to use that specific provider," not "required globally." You only need at least one provider configured.
|
||||
|
||||
### ANTHROPIC_API_KEY (Recommended)
|
||||
- **Provider**: Anthropic Claude models
|
||||
- **Format**: `sk-ant-api03-...`
|
||||
- **Required**: ✅ **Yes**
|
||||
- **Models**: Claude 3.5 Sonnet, Claude 3 Haiku, Claude 3 Opus
|
||||
- **Get Key**: [Anthropic Console](https://console.anthropic.com/)
|
||||
|
||||
```bash
|
||||
ANTHROPIC_API_KEY="sk-ant-api03-your-key-here"
|
||||
```
|
||||
|
||||
### PERPLEXITY_API_KEY (Highly Recommended for Research)
|
||||
- **Provider**: Perplexity AI (Research features)
|
||||
- **Format**: `pplx-...`
|
||||
- **Required**: ✅ **Yes**
|
||||
- **Purpose**: Enables research-backed task expansions and updates
|
||||
- **Models**: Perplexity Sonar models
|
||||
- **Get Key**: [Perplexity API](https://www.perplexity.ai/settings/api)
|
||||
|
||||
```bash
|
||||
PERPLEXITY_API_KEY="pplx-your-key-here"
|
||||
```
|
||||
|
||||
### OPENAI_API_KEY
|
||||
- **Provider**: OpenAI GPT models
|
||||
- **Format**: `sk-proj-...` or `sk-...`
|
||||
- **Required**: ✅ **Yes**
|
||||
- **Models**: GPT-4, GPT-4 Turbo, GPT-3.5 Turbo, O1 models
|
||||
- **Get Key**: [OpenAI Platform](https://platform.openai.com/api-keys)
|
||||
|
||||
```bash
|
||||
OPENAI_API_KEY="sk-proj-your-key-here"
|
||||
```
|
||||
|
||||
### GOOGLE_API_KEY
|
||||
- **Provider**: Google Gemini models
|
||||
- **Format**: Various formats
|
||||
- **Required**: ✅ **Yes**
|
||||
- **Models**: Gemini Pro, Gemini Flash, Gemini Ultra
|
||||
- **Get Key**: [Google AI Studio](https://aistudio.google.com/app/apikey)
|
||||
- **Alternative**: Use `GOOGLE_APPLICATION_CREDENTIALS` for service account (Google Vertex)
|
||||
|
||||
```bash
|
||||
GOOGLE_API_KEY="your-google-api-key-here"
|
||||
```
|
||||
|
||||
### GROQ_API_KEY
|
||||
- **Provider**: Groq (High-performance inference)
|
||||
- **Required**: ✅ **Yes**
|
||||
- **Models**: Llama models, Mixtral models (via Groq)
|
||||
- **Get Key**: [Groq Console](https://console.groq.com/keys)
|
||||
|
||||
```bash
|
||||
GROQ_API_KEY="your-groq-key-here"
|
||||
```
|
||||
|
||||
### OPENROUTER_API_KEY
|
||||
- **Provider**: OpenRouter (Multiple model access)
|
||||
- **Required**: ✅ **Yes**
|
||||
- **Models**: Access to various models through single API
|
||||
- **Get Key**: [OpenRouter](https://openrouter.ai/keys)
|
||||
|
||||
```bash
|
||||
OPENROUTER_API_KEY="your-openrouter-key-here"
|
||||
```
|
||||
|
||||
### AZURE_OPENAI_API_KEY
|
||||
- **Provider**: Azure OpenAI Service
|
||||
- **Required**: ✅ **Yes**
|
||||
- **Requirements**: Also requires `AZURE_OPENAI_ENDPOINT` configuration
|
||||
- **Models**: GPT models via Azure
|
||||
- **Get Key**: [Azure Portal](https://portal.azure.com/)
|
||||
|
||||
```bash
|
||||
AZURE_OPENAI_API_KEY="your-azure-key-here"
|
||||
```
|
||||
|
||||
### XAI_API_KEY
|
||||
- **Provider**: xAI (Grok) models
|
||||
- **Required**: ✅ **Yes**
|
||||
- **Models**: Grok models
|
||||
- **Get Key**: [xAI Console](https://console.x.ai/)
|
||||
|
||||
```bash
|
||||
XAI_API_KEY="your-xai-key-here"
|
||||
```
|
||||
|
||||
## Optional API Keys
|
||||
|
||||
> **Note**: These API keys are optional - providers will work without them or use alternative authentication methods.
|
||||
|
||||
### AWS_ACCESS_KEY_ID (Bedrock)
|
||||
- **Provider**: AWS Bedrock
|
||||
- **Required**: ❌ **No** (uses AWS credential chain)
|
||||
- **Models**: Claude models via AWS Bedrock
|
||||
- **Authentication**: Uses AWS credential chain (profiles, IAM roles, etc.)
|
||||
- **Get Key**: [AWS Console](https://console.aws.amazon.com/iam/)
|
||||
|
||||
```bash
|
||||
# Optional - AWS credential chain is preferred
|
||||
AWS_ACCESS_KEY_ID="your-aws-access-key"
|
||||
AWS_SECRET_ACCESS_KEY="your-aws-secret-key"
|
||||
```
|
||||
|
||||
### CLAUDE_CODE_API_KEY
|
||||
- **Provider**: Claude Code CLI
|
||||
- **Required**: ❌ **No** (uses OAuth tokens)
|
||||
- **Purpose**: Integration with local Claude Code CLI
|
||||
- **Authentication**: Uses OAuth tokens, no API key needed
|
||||
|
||||
```bash
|
||||
# Not typically needed
|
||||
CLAUDE_CODE_API_KEY="not-usually-required"
|
||||
```
|
||||
|
||||
### GEMINI_API_KEY
|
||||
- **Provider**: Gemini CLI
|
||||
- **Required**: ❌ **No** (uses OAuth authentication)
|
||||
- **Purpose**: Integration with Gemini CLI
|
||||
- **Authentication**: Primarily uses OAuth via CLI, API key is optional
|
||||
|
||||
```bash
|
||||
# Optional - OAuth via CLI is preferred
|
||||
GEMINI_API_KEY="your-gemini-key-here"
|
||||
```
|
||||
|
||||
### GROK_CLI_API_KEY
|
||||
- **Provider**: Grok CLI
|
||||
- **Required**: ❌ **No** (can use CLI config)
|
||||
- **Purpose**: Integration with Grok CLI
|
||||
- **Authentication**: Can use Grok CLI's own config file
|
||||
|
||||
```bash
|
||||
# Optional - CLI config is preferred
|
||||
GROK_CLI_API_KEY="your-grok-cli-key"
|
||||
```
|
||||
|
||||
### OLLAMA_API_KEY
|
||||
- **Provider**: Ollama (Local/Remote)
|
||||
- **Required**: ❌ **No** (local installation doesn't need key)
|
||||
- **Purpose**: For remote Ollama servers that require authentication
|
||||
- **Requirements**: Only needed for remote servers with authentication
|
||||
- **Note**: Not needed for local Ollama installations
|
||||
|
||||
```bash
|
||||
# Only needed for remote Ollama servers
|
||||
OLLAMA_API_KEY="your-ollama-api-key-here"
|
||||
```
|
||||
|
||||
### GITHUB_API_KEY
|
||||
- **Provider**: GitHub (Import/Export features)
|
||||
- **Format**: `ghp_...` or `github_pat_...`
|
||||
- **Required**: ❌ **No** (for GitHub features only)
|
||||
- **Purpose**: GitHub import/export features
|
||||
- **Get Key**: [GitHub Settings](https://github.com/settings/tokens)
|
||||
|
||||
```bash
|
||||
GITHUB_API_KEY="ghp-your-github-key-here"
|
||||
```
|
||||
|
||||
## Configuration Methods
|
||||
|
||||
### Method 1: Environment File (.env)
|
||||
Create a `.env` file in your project root:
|
||||
|
||||
```bash
|
||||
# Copy from .env.example
|
||||
cp .env.example .env
|
||||
|
||||
# Edit with your keys
|
||||
vim .env
|
||||
```
|
||||
|
||||
### Method 2: System Environment Variables
|
||||
```bash
|
||||
export ANTHROPIC_API_KEY="your-key-here"
|
||||
export PERPLEXITY_API_KEY="your-key-here"
|
||||
# ... other keys
|
||||
```
|
||||
|
||||
### Method 3: MCP Server Configuration
|
||||
For Claude Code integration, configure keys in `.mcp.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"task-master-ai": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "task-master-ai"],
|
||||
"env": {
|
||||
"ANTHROPIC_API_KEY": "your-key-here",
|
||||
"PERPLEXITY_API_KEY": "your-key-here",
|
||||
"OPENAI_API_KEY": "your-key-here"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Key Requirements
|
||||
|
||||
### Minimum Requirements
|
||||
- **At least one** AI provider key is required
|
||||
- **ANTHROPIC_API_KEY** is recommended as the primary provider
|
||||
- **PERPLEXITY_API_KEY** is highly recommended for research features
|
||||
|
||||
### Provider-Specific Requirements
|
||||
- **Azure OpenAI**: Requires both `AZURE_OPENAI_API_KEY` and `AZURE_OPENAI_ENDPOINT` configuration
|
||||
- **Google Vertex**: Requires `VERTEX_PROJECT_ID` and `VERTEX_LOCATION` environment variables
|
||||
- **AWS Bedrock**: Uses AWS credential chain (profiles, IAM roles, etc.) instead of API keys
|
||||
- **Ollama**: Only needs API key for remote servers with authentication
|
||||
- **CLI Providers**: Gemini CLI, Grok CLI, and Claude Code use OAuth/CLI config instead of API keys
|
||||
|
||||
## Model Configuration
|
||||
|
||||
After setting up API keys, configure which models to use:
|
||||
|
||||
```bash
|
||||
# Interactive model setup
|
||||
task-master models --setup
|
||||
|
||||
# Set specific models
|
||||
task-master models --set-main claude-3-5-sonnet-20241022
|
||||
task-master models --set-research perplexity-llama-3.1-sonar-large-128k-online
|
||||
task-master models --set-fallback gpt-4o-mini
|
||||
```
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
1. **Never commit API keys** to version control
|
||||
2. **Use .env files** and add them to `.gitignore`
|
||||
3. **Rotate keys regularly** especially if compromised
|
||||
4. **Use minimal permissions** for service accounts
|
||||
5. **Monitor usage** to detect unauthorized access
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Key Validation
|
||||
```bash
|
||||
# Check if keys are properly configured
|
||||
task-master models
|
||||
|
||||
# Test specific provider
|
||||
task-master add-task --prompt="test task" --model=claude-3-5-sonnet-20241022
|
||||
```
|
||||
|
||||
### Common Issues
|
||||
- **Invalid key format**: Check the expected format for each provider
|
||||
- **Insufficient permissions**: Ensure keys have necessary API access
|
||||
- **Rate limits**: Some providers have usage limits
|
||||
- **Regional restrictions**: Some models may not be available in all regions
|
||||
|
||||
### Getting Help
|
||||
If you encounter issues with API key configuration:
|
||||
- Check the [FAQ](/getting-started/faq) for common solutions
|
||||
- Join our [Discord community](https://discord.gg/fWJkU7rf) for support
|
||||
- Report issues on [GitHub](https://github.com/eyaltoledano/claude-task-master/issues)
|
||||
@@ -18,8 +18,8 @@ For MCP/Cursor usage: Configure keys in the env section of your .cursor/mcp.json
|
||||
{
|
||||
"mcpServers": {
|
||||
"task-master-ai": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "task-master-ai"],
|
||||
"command": "node",
|
||||
"args": ["./mcp-server/server.js"],
|
||||
"env": {
|
||||
"ANTHROPIC_API_KEY": "ANTHROPIC_API_KEY_HERE",
|
||||
"PERPLEXITY_API_KEY": "PERPLEXITY_API_KEY_HERE",
|
||||
@@ -108,5 +108,5 @@ You don’t need to configure everything up front. Most settings can be left as
|
||||
</Accordion>
|
||||
|
||||
<Note>
|
||||
For advanced configuration options and detailed customization, see our [Advanced Configuration Guide](/best-practices/configuration-advanced) page.
|
||||
For advanced configuration options and detailed customization, see our [Advanced Configuration Guide](/docs/best-practices/configuration-advanced) page.
|
||||
</Note>
|
||||
@@ -56,4 +56,4 @@ If you ran into problems and had to debug errors you can create new rules as you
|
||||
|
||||
By now you have all you need to get started executing code faster and smarter with Task Master.
|
||||
|
||||
If you have any questions please check out [Frequently Asked Questions](/getting-started/faq)
|
||||
If you have any questions please check out [Frequently Asked Questions](/docs/getting-started/faq)
|
||||
|
||||
@@ -30,19 +30,6 @@ cursor://anysphere.cursor-deeplink/mcp/install?name=taskmaster-ai&config=eyJjb21
|
||||
```
|
||||
|
||||
> **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.
|
||||
|
||||
### Claude Code Quick Install
|
||||
|
||||
For Claude Code users:
|
||||
|
||||
```bash
|
||||
claude mcp add taskmaster-ai -- npx -y task-master-ai
|
||||
```
|
||||
|
||||
Don't forget to add your API keys to the configuration:
|
||||
- in the root .env of your Project
|
||||
- in the "env" section of your mcp config for taskmaster-ai
|
||||
|
||||
</Accordion>
|
||||
## Installation Options
|
||||
|
||||
@@ -69,7 +56,7 @@ MCP (Model Control Protocol) lets you run Task Master directly from your editor.
|
||||
"mcpServers": {
|
||||
"taskmaster-ai": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "task-master-ai"],
|
||||
"args": ["-y", "--package=task-master-ai", "task-master-ai"],
|
||||
"env": {
|
||||
"ANTHROPIC_API_KEY": "YOUR_ANTHROPIC_API_KEY_HERE",
|
||||
"PERPLEXITY_API_KEY": "YOUR_PERPLEXITY_API_KEY_HERE",
|
||||
@@ -88,7 +75,7 @@ MCP (Model Control Protocol) lets you run Task Master directly from your editor.
|
||||
|
||||
> 🔑 Replace `YOUR_…_KEY_HERE` with your real API keys. You can remove keys you don't use.
|
||||
|
||||
> **Note**: If you see `0 tools enabled` in the MCP settings, restart your editor and check that your API keys are correctly configured.
|
||||
> **Note**: If you see `0 tools enabled` in the MCP settings, try removing the `--package=task-master-ai` flag from `args`.
|
||||
|
||||
### VS Code (`servers` + `type`)
|
||||
|
||||
@@ -97,7 +84,7 @@ MCP (Model Control Protocol) lets you run Task Master directly from your editor.
|
||||
"servers": {
|
||||
"taskmaster-ai": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "task-master-ai"],
|
||||
"args": ["-y", "--package=task-master-ai", "task-master-ai"],
|
||||
"env": {
|
||||
"ANTHROPIC_API_KEY": "YOUR_ANTHROPIC_API_KEY_HERE",
|
||||
"PERPLEXITY_API_KEY": "YOUR_PERPLEXITY_API_KEY_HERE",
|
||||
|
||||
@@ -6,13 +6,13 @@ sidebarTitle: "Quick Start"
|
||||
This guide is for new users who want to start using Task Master with minimal setup time.
|
||||
|
||||
It covers:
|
||||
- [Requirements](/getting-started/quick-start/requirements): You will need Node.js and an AI model API Key.
|
||||
- [Installation](/getting-started/quick-start/installation): How to Install Task Master.
|
||||
- [Configuration](/getting-started/quick-start/configuration-quick): Setting up your API Key, MCP, and more.
|
||||
- [PRD](/getting-started/quick-start/prd-quick): Writing and parsing your first PRD.
|
||||
- [Task Setup](/getting-started/quick-start/tasks-quick): Preparing your tasks for execution.
|
||||
- [Executing Tasks](/getting-started/quick-start/execute-quick): Using Task Master to execute tasks.
|
||||
- [Rules & Context](/getting-started/quick-start/rules-quick): Learn how and why to build context in your project over time.
|
||||
- [Requirements](/docs/getting-started/quick-start/requirements): You will need Node.js and an AI model API Key.
|
||||
- [Installation](/docs/getting-started/quick-start/installation): How to Install Task Master.
|
||||
- [Configuration](/docs/getting-started/quick-start/configuration-quick): Setting up your API Key, MCP, and more.
|
||||
- [PRD](/docs/getting-started/quick-start/prd-quick): Writing and parsing your first PRD.
|
||||
- [Task Setup](/docs/getting-started/quick-start/tasks-quick): Preparing your tasks for execution.
|
||||
- [Executing Tasks](/docs/getting-started/quick-start/execute-quick): Using Task Master to execute tasks.
|
||||
- [Rules & Context](/docs/getting-started/quick-start/rules-quick): Learn how and why to build context in your project over time.
|
||||
|
||||
<Tip>
|
||||
By the end of this guide, you'll have everything you need to begin working productively with Task Master.
|
||||
|
||||
@@ -61,25 +61,9 @@ Task Master can provide a complexity report which can be helpful to read before
|
||||
Can you analyze the complexity of our tasks to help me understand which ones need to be broken down further?
|
||||
```
|
||||
|
||||
The agent will use the `analyze_project_complexity` MCP tool, or you can run it directly with the CLI command:
|
||||
```bash
|
||||
task-master analyze-complexity
|
||||
```
|
||||
|
||||
For more comprehensive analysis using your configured research model, you can use:
|
||||
```bash
|
||||
task-master analyze-complexity --research
|
||||
```
|
||||
|
||||
<Tip>
|
||||
The `--research` flag uses whatever research model you have configured in `.taskmaster/config.json` (configurable via `task-master models --setup`) for research-backed complexity analysis, providing more informed recommendations.
|
||||
</Tip>
|
||||
|
||||
You can view the report in a friendly table using:
|
||||
```
|
||||
Can you show me the complexity report in a more readable format?
|
||||
```
|
||||
|
||||
For more detailed CLI options, see the [Analyze Task Complexity](/capabilities/cli-root-commands#analyze-task-complexity) section.
|
||||
|
||||
<Check>Now you are ready to begin [executing tasks](/getting-started/quick-start/execute-quick)</Check>
|
||||
<Check>Now you are ready to begin [executing tasks](/docs/getting-started/quick-start/execute-quick)</Check>
|
||||
@@ -4,7 +4,7 @@ Welcome to v1 of the Task Master Docs. Expect weekly updates as we expand and re
|
||||
|
||||
We've organized the docs into three sections depending on your experience level and goals:
|
||||
|
||||
### Getting Started - Jump in to [Quick Start](/getting-started/quick-start)
|
||||
### Getting Started - Jump in to [Quick Start](/docs/getting-started/quick-start)
|
||||
Designed for first-time users. Get set up, create your first PRD, and run your first task.
|
||||
|
||||
### Best Practices
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "docs",
|
||||
"version": "0.0.4",
|
||||
"version": "0.0.2",
|
||||
"private": true,
|
||||
"description": "Task Master documentation powered by Mintlify",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,68 +1,5 @@
|
||||
# Change Log
|
||||
|
||||
## 0.25.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`af53525`](https://github.com/eyaltoledano/claude-task-master/commit/af53525cbc660a595b67d4bb90d906911c71f45d)]:
|
||||
- task-master-ai@0.27.3
|
||||
|
||||
## 0.25.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`044a7bf`](https://github.com/eyaltoledano/claude-task-master/commit/044a7bfc98049298177bc655cf341d7a8b6a0011)]:
|
||||
- task-master-ai@0.27.2
|
||||
|
||||
## 0.25.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`f487736`](https://github.com/eyaltoledano/claude-task-master/commit/f487736670ef8c484059f676293777eabb249c9e), [`c911608`](https://github.com/eyaltoledano/claude-task-master/commit/c911608f60454253f4e024b57ca84e5a5a53f65c), [`1a18794`](https://github.com/eyaltoledano/claude-task-master/commit/1a1879483b86c118a4e46c02cbf4acebfcf6bcf9)]:
|
||||
- task-master-ai@0.27.1
|
||||
|
||||
## 0.25.2-rc.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`1a18794`](https://github.com/eyaltoledano/claude-task-master/commit/1a1879483b86c118a4e46c02cbf4acebfcf6bcf9)]:
|
||||
- task-master-ai@0.27.1-rc.1
|
||||
|
||||
## 0.25.2-rc.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`f487736`](https://github.com/eyaltoledano/claude-task-master/commit/f487736670ef8c484059f676293777eabb249c9e)]:
|
||||
- task-master-ai@0.27.1-rc.0
|
||||
|
||||
## 0.25.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#1200](https://github.com/eyaltoledano/claude-task-master/pull/1200) [`fce8414`](https://github.com/eyaltoledano/claude-task-master/commit/fce841490a9ebbf1801a42dd8a29397379cf1142) Thanks [@eyaltoledano](https://github.com/eyaltoledano)! - Add "Start Task" button to VS Code extension for seamless Claude Code integration
|
||||
|
||||
You can now click a "Start Task" button directly in the Task Master extension which will open a new terminal and automatically execute the task using Claude Code. This provides a seamless workflow from viewing tasks in the extension to implementing them without leaving VS Code.
|
||||
|
||||
- [#1201](https://github.com/eyaltoledano/claude-task-master/pull/1201) [`83af314`](https://github.com/eyaltoledano/claude-task-master/commit/83af314879fc0e563581161c60d2bd089899313e) Thanks [@losolosol](https://github.com/losolosol)! - Added a Start Build button to the VSCODE Task Properties Right Panel
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#1229](https://github.com/eyaltoledano/claude-task-master/pull/1229) [`674d1f6`](https://github.com/eyaltoledano/claude-task-master/commit/674d1f6de7ea98116b61bdae6198bafe6c4e7c1a) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fix MCP not connecting to new Taskmaster version
|
||||
|
||||
- Updated dependencies [[`4e12643`](https://github.com/eyaltoledano/claude-task-master/commit/4e126430a092fb54afb035514fb3d46115714f97), [`fce8414`](https://github.com/eyaltoledano/claude-task-master/commit/fce841490a9ebbf1801a42dd8a29397379cf1142), [`fce8414`](https://github.com/eyaltoledano/claude-task-master/commit/fce841490a9ebbf1801a42dd8a29397379cf1142), [`fce8414`](https://github.com/eyaltoledano/claude-task-master/commit/fce841490a9ebbf1801a42dd8a29397379cf1142), [`a621ff0`](https://github.com/eyaltoledano/claude-task-master/commit/a621ff05eafb51a147a9aabd7b37ddc0e45b0869), [`e6de285`](https://github.com/eyaltoledano/claude-task-master/commit/e6de285ceacb0a397e952a63435cd32a9c731515), [`fce8414`](https://github.com/eyaltoledano/claude-task-master/commit/fce841490a9ebbf1801a42dd8a29397379cf1142)]:
|
||||
- task-master-ai@0.27.0
|
||||
|
||||
## 0.25.0-rc.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#1201](https://github.com/eyaltoledano/claude-task-master/pull/1201) [`83af314`](https://github.com/eyaltoledano/claude-task-master/commit/83af314879fc0e563581161c60d2bd089899313e) Thanks [@losolosol](https://github.com/losolosol)! - Added a Start Build button to the VSCODE Task Properties Right Panel
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`137ef36`](https://github.com/eyaltoledano/claude-task-master/commit/137ef362789a9cdfdb1925e35e0438c1fa6c69ee)]:
|
||||
- task-master-ai@0.27.0-rc.0
|
||||
|
||||
## 0.24.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -109,7 +109,7 @@ Access settings via **File → Preferences → Settings** and search for "Taskma
|
||||
|
||||
### **MCP Connection Settings**
|
||||
- **MCP Server Command** - Path to task-master-ai executable (default: `npx`)
|
||||
- **MCP Server Args** - Arguments for the server command (default: `-y`, `task-master-ai`)
|
||||
- **MCP Server Args** - Arguments for the server command (default: `-y`, `--package=task-master-ai`, `task-master-ai`)
|
||||
- **Connection Timeout** - Server response timeout (default: 30s)
|
||||
- **Auto Refresh** - Enable automatic task updates (default: enabled)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"private": true,
|
||||
"displayName": "TaskMaster",
|
||||
"description": "A visual Kanban board interface for TaskMaster projects in VS Code",
|
||||
"version": "0.25.4",
|
||||
"version": "0.24.2",
|
||||
"publisher": "Hamster",
|
||||
"icon": "assets/icon.png",
|
||||
"engines": {
|
||||
@@ -240,7 +240,7 @@
|
||||
"check-types": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"task-master-ai": "*"
|
||||
"task-master-ai": "0.26.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
@@ -254,9 +254,8 @@
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@tailwindcss/postcss": "^4.1.11",
|
||||
"@tanstack/react-query": "^5.83.0",
|
||||
"@types/mocha": "^10.0.10",
|
||||
"@types/node": "^22.10.5",
|
||||
"@types/node": "20.x",
|
||||
"@types/react": "19.1.8",
|
||||
"@types/react-dom": "19.1.6",
|
||||
"@types/vscode": "^1.101.0",
|
||||
@@ -272,12 +271,12 @@
|
||||
"lucide-react": "^0.525.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"postcss": "8.5.6",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss": "4.1.11",
|
||||
"typescript": "^5.9.2",
|
||||
"@tm/core": "*"
|
||||
"typescript": "^5.8.3",
|
||||
"@tanstack/react-query": "^5.83.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"overrides": {
|
||||
"glob@<8": "^10.4.5",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "task-master-hamster",
|
||||
"displayName": "Taskmaster AI",
|
||||
"description": "A visual Kanban board interface for Taskmaster projects in VS Code",
|
||||
"version": "0.25.3",
|
||||
"version": "0.23.1",
|
||||
"publisher": "Hamster",
|
||||
"icon": "assets/icon.png",
|
||||
"engines": {
|
||||
@@ -95,7 +95,7 @@
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"default": ["-y", "task-master-ai"],
|
||||
"default": ["-y", "--package=task-master-ai", "task-master-ai"],
|
||||
"description": "An array of arguments to pass to the MCP server command."
|
||||
},
|
||||
"taskmaster.mcp.cwd": {
|
||||
|
||||
@@ -11,6 +11,7 @@ interface TaskMetadataSidebarProps {
|
||||
tasks: TaskMasterTask[];
|
||||
complexity: any;
|
||||
isSubtask: boolean;
|
||||
sendMessage: (message: any) => Promise<any>;
|
||||
onStatusChange: (status: TaskMasterTask['status']) => void;
|
||||
onDependencyClick: (depId: string) => void;
|
||||
isRegenerating?: boolean;
|
||||
@@ -22,12 +23,13 @@ export const TaskMetadataSidebar: React.FC<TaskMetadataSidebarProps> = ({
|
||||
tasks,
|
||||
complexity,
|
||||
isSubtask,
|
||||
sendMessage,
|
||||
onStatusChange,
|
||||
onDependencyClick,
|
||||
isRegenerating = false,
|
||||
isAppending = false
|
||||
}) => {
|
||||
const { sendMessage } = useVSCodeContext();
|
||||
const { vscode } = useVSCodeContext();
|
||||
const [isLoadingComplexity, setIsLoadingComplexity] = useState(false);
|
||||
const [mcpComplexityScore, setMcpComplexityScore] = useState<
|
||||
number | undefined
|
||||
@@ -99,37 +101,26 @@ export const TaskMetadataSidebar: React.FC<TaskMetadataSidebarProps> = ({
|
||||
};
|
||||
|
||||
// Handle starting a task
|
||||
const handleStartTask = async () => {
|
||||
const handleStartTask = () => {
|
||||
if (!currentTask || isStartingTask) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsStartingTask(true);
|
||||
|
||||
try {
|
||||
// Send message to extension to open terminal
|
||||
const result = await sendMessage({
|
||||
// Send message to extension to open terminal
|
||||
if (vscode) {
|
||||
vscode.postMessage({
|
||||
type: 'openTerminal',
|
||||
data: {
|
||||
taskId: currentTask.id,
|
||||
taskTitle: currentTask.title
|
||||
}
|
||||
taskId: currentTask.id,
|
||||
taskTitle: currentTask.title
|
||||
});
|
||||
|
||||
// Handle the response
|
||||
if (result && !result.success) {
|
||||
console.error('Terminal execution failed:', result.error);
|
||||
// The extension will show VS Code error notification and webview toast
|
||||
} else if (result && result.success) {
|
||||
console.log('Terminal started successfully:', result.terminalName);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to start task:', error);
|
||||
// This handles network/communication errors
|
||||
} finally {
|
||||
// Reset loading state
|
||||
setIsStartingTask(false);
|
||||
}
|
||||
|
||||
// Reset loading state after a short delay
|
||||
setTimeout(() => {
|
||||
setIsStartingTask(false);
|
||||
}, 500);
|
||||
};
|
||||
|
||||
// Effect to handle complexity on task change
|
||||
|
||||
@@ -208,6 +208,7 @@ export const TaskDetailsView: React.FC<TaskDetailsViewProps> = ({
|
||||
tasks={allTasks}
|
||||
complexity={complexity}
|
||||
isSubtask={isSubtask}
|
||||
sendMessage={sendMessage}
|
||||
onStatusChange={handleStatusChange}
|
||||
onDependencyClick={handleDependencyClick}
|
||||
/>
|
||||
|
||||
@@ -8,7 +8,6 @@ import { ConfigService } from './services/config-service';
|
||||
import { PollingService } from './services/polling-service';
|
||||
import { createPollingStrategy } from './services/polling-strategies';
|
||||
import { TaskRepository } from './services/task-repository';
|
||||
import { TerminalManager } from './services/terminal-manager';
|
||||
import { WebviewManager } from './services/webview-manager';
|
||||
import { EventEmitter } from './utils/event-emitter';
|
||||
import { ExtensionLogger } from './utils/logger';
|
||||
@@ -23,7 +22,6 @@ let logger: ExtensionLogger;
|
||||
let mcpClient: MCPClientManager;
|
||||
let api: TaskMasterApi;
|
||||
let repository: TaskRepository;
|
||||
let terminalManager: TerminalManager;
|
||||
let pollingService: PollingService;
|
||||
let webviewManager: WebviewManager;
|
||||
let events: EventEmitter;
|
||||
@@ -48,9 +46,6 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
// Repository with caching (actually useful for performance)
|
||||
repository = new TaskRepository(api, logger);
|
||||
|
||||
// Terminal manager for task execution
|
||||
terminalManager = new TerminalManager(context, logger);
|
||||
|
||||
// Config service for TaskMaster config.json
|
||||
configService = new ConfigService(logger);
|
||||
|
||||
@@ -61,13 +56,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
pollingService = new PollingService(repository, strategy, logger);
|
||||
|
||||
// Webview manager (cleaner than global panel array) - create before connection
|
||||
webviewManager = new WebviewManager(
|
||||
context,
|
||||
repository,
|
||||
events,
|
||||
logger,
|
||||
terminalManager
|
||||
);
|
||||
webviewManager = new WebviewManager(context, repository, events, logger);
|
||||
webviewManager.setConfigService(configService);
|
||||
|
||||
// Sidebar webview manager
|
||||
@@ -221,11 +210,10 @@ function registerCommands(context: vscode.ExtensionContext) {
|
||||
);
|
||||
}
|
||||
|
||||
export async function deactivate() {
|
||||
export function deactivate() {
|
||||
logger?.log('👋 TaskMaster Extension deactivating...');
|
||||
pollingService?.stop();
|
||||
webviewManager?.dispose();
|
||||
await terminalManager?.dispose();
|
||||
api?.destroy();
|
||||
mcpClient?.disconnect();
|
||||
}
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
/**
|
||||
* Terminal Manager - Handles task execution in VS Code terminals
|
||||
* Uses @tm/core for consistent task management with the CLI
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { createTaskMasterCore, type TaskMasterCore } from '@tm/core';
|
||||
import type { ExtensionLogger } from '../utils/logger';
|
||||
|
||||
export interface TerminalExecutionOptions {
|
||||
taskId: string;
|
||||
taskTitle: string;
|
||||
tag?: string;
|
||||
}
|
||||
|
||||
export interface TerminalExecutionResult {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
terminalName?: string;
|
||||
}
|
||||
|
||||
export class TerminalManager {
|
||||
private terminals = new Map<string, vscode.Terminal>();
|
||||
private tmCore?: TaskMasterCore;
|
||||
|
||||
constructor(
|
||||
private context: vscode.ExtensionContext,
|
||||
private logger: ExtensionLogger
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Execute a task in a new VS Code terminal with Claude
|
||||
* Uses @tm/core for consistent task management with the CLI
|
||||
*/
|
||||
async executeTask(
|
||||
options: TerminalExecutionOptions
|
||||
): Promise<TerminalExecutionResult> {
|
||||
const { taskTitle, tag } = options;
|
||||
// Ensure taskId is always a string
|
||||
const taskId = String(options.taskId);
|
||||
|
||||
this.logger.log(
|
||||
`Starting task execution for ${taskId}: ${taskTitle}${tag ? ` (tag: ${tag})` : ''}`
|
||||
);
|
||||
this.logger.log(`TaskId type: ${typeof taskId}, value: ${taskId}`);
|
||||
|
||||
try {
|
||||
// Initialize tm-core if needed
|
||||
await this.initializeCore();
|
||||
|
||||
// Use tm-core to start the task (same as CLI)
|
||||
const startResult = await this.tmCore!.startTask(taskId, {
|
||||
dryRun: false,
|
||||
force: false,
|
||||
updateStatus: true
|
||||
});
|
||||
|
||||
if (!startResult.started || !startResult.executionOutput) {
|
||||
throw new Error(
|
||||
startResult.error || 'Failed to start task with tm-core'
|
||||
);
|
||||
}
|
||||
|
||||
// Create terminal with custom TaskMaster icon
|
||||
const terminalName = `Task ${taskId}: ${taskTitle}`;
|
||||
const terminal = this.createTerminal(terminalName);
|
||||
|
||||
// Store terminal reference for potential cleanup
|
||||
this.terminals.set(taskId, terminal);
|
||||
|
||||
// Show terminal and run Claude command
|
||||
terminal.show();
|
||||
const command = `claude "${startResult.executionOutput}"`;
|
||||
terminal.sendText(command);
|
||||
|
||||
this.logger.log(`Launched Claude for task ${taskId} using tm-core`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
terminalName
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to execute task:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new terminal with TaskMaster branding
|
||||
*/
|
||||
private createTerminal(name: string): vscode.Terminal {
|
||||
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||
|
||||
return vscode.window.createTerminal({
|
||||
name,
|
||||
cwd: workspaceRoot,
|
||||
iconPath: new vscode.ThemeIcon('play') // Use a VS Code built-in icon for now
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize TaskMaster Core (same as CLI)
|
||||
*/
|
||||
private async initializeCore(): Promise<void> {
|
||||
if (!this.tmCore) {
|
||||
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||
if (!workspaceRoot) {
|
||||
throw new Error('No workspace folder found');
|
||||
}
|
||||
this.tmCore = await createTaskMasterCore({ projectPath: workspaceRoot });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get terminal by task ID (if still active)
|
||||
*/
|
||||
getTerminalByTaskId(taskId: string): vscode.Terminal | undefined {
|
||||
return this.terminals.get(taskId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up terminated terminals
|
||||
*/
|
||||
cleanupTerminal(taskId: string): void {
|
||||
const terminal = this.terminals.get(taskId);
|
||||
if (terminal) {
|
||||
this.terminals.delete(taskId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose all managed terminals and clean up tm-core
|
||||
*/
|
||||
async dispose(): Promise<void> {
|
||||
this.terminals.forEach((terminal) => {
|
||||
try {
|
||||
terminal.dispose();
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to dispose terminal:', error);
|
||||
}
|
||||
});
|
||||
this.terminals.clear();
|
||||
|
||||
if (this.tmCore) {
|
||||
try {
|
||||
await this.tmCore.close();
|
||||
this.tmCore = undefined;
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to close tm-core:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import type { EventEmitter } from '../utils/event-emitter';
|
||||
import type { ExtensionLogger } from '../utils/logger';
|
||||
import type { ConfigService } from './config-service';
|
||||
import type { TaskRepository } from './task-repository';
|
||||
import type { TerminalManager } from './terminal-manager';
|
||||
|
||||
export class WebviewManager {
|
||||
private panels = new Set<vscode.WebviewPanel>();
|
||||
@@ -20,8 +19,7 @@ export class WebviewManager {
|
||||
private context: vscode.ExtensionContext,
|
||||
private repository: TaskRepository,
|
||||
private events: EventEmitter,
|
||||
private logger: ExtensionLogger,
|
||||
private terminalManager: TerminalManager
|
||||
private logger: ExtensionLogger
|
||||
) {}
|
||||
|
||||
setConfigService(configService: ConfigService): void {
|
||||
@@ -364,67 +362,27 @@ export class WebviewManager {
|
||||
return;
|
||||
|
||||
case 'openTerminal':
|
||||
// Delegate terminal execution to TerminalManager
|
||||
const { taskId, taskTitle } = data.data || data; // Handle both nested and direct data
|
||||
// Open VS Code terminal for task execution
|
||||
this.logger.log(
|
||||
`Webview openTerminal - taskId: ${taskId} (type: ${typeof taskId}), taskTitle: ${taskTitle}`
|
||||
`Opening terminal for task ${data.taskId}: ${data.taskTitle}`
|
||||
);
|
||||
|
||||
// Get current tag to ensure we're working in the right context
|
||||
let currentTag = 'master'; // default fallback
|
||||
if (this.mcpClient) {
|
||||
try {
|
||||
const tagsResult = await this.mcpClient.callTool('list_tags', {
|
||||
projectRoot: vscode.workspace.workspaceFolders?.[0]?.uri.fsPath,
|
||||
showMetadata: false
|
||||
});
|
||||
try {
|
||||
const terminal = vscode.window.createTerminal({
|
||||
name: `Task ${data.taskId}: ${data.taskTitle}`,
|
||||
cwd: vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
|
||||
});
|
||||
terminal.show();
|
||||
|
||||
let parsedData;
|
||||
if (
|
||||
tagsResult?.content &&
|
||||
Array.isArray(tagsResult.content) &&
|
||||
tagsResult.content[0]?.text
|
||||
) {
|
||||
try {
|
||||
parsedData = JSON.parse(tagsResult.content[0].text);
|
||||
if (parsedData?.data?.currentTag) {
|
||||
currentTag = parsedData.data.currentTag;
|
||||
}
|
||||
} catch (e) {
|
||||
this.logger.warn(
|
||||
'Failed to parse tags response for terminal execution'
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
'Failed to get current tag for terminal execution:',
|
||||
error
|
||||
);
|
||||
}
|
||||
this.logger.log('Terminal created and shown successfully');
|
||||
response = { success: true };
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to create terminal:', error);
|
||||
response = {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
};
|
||||
}
|
||||
|
||||
const result = await this.terminalManager.executeTask({
|
||||
taskId,
|
||||
taskTitle,
|
||||
tag: currentTag
|
||||
});
|
||||
|
||||
response = result;
|
||||
|
||||
// Show user feedback AFTER sending the response (like the working "TaskMaster connected!" example)
|
||||
setImmediate(() => {
|
||||
if (result.success) {
|
||||
// Success: Show info message
|
||||
vscode.window.showInformationMessage(
|
||||
`✅ Started Claude session for Task ${taskId}: ${taskTitle}`
|
||||
);
|
||||
} else {
|
||||
// Error: Show VS Code native error notification only
|
||||
const errorMsg = `Failed to start task: ${result.error}`;
|
||||
vscode.window.showErrorMessage(errorMsg);
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
|
||||
@@ -408,7 +408,7 @@ export function createMCPConfigFromSettings(): MCPConfig {
|
||||
const taskMasterPath = require.resolve('task-master-ai');
|
||||
const mcpServerPath = path.resolve(
|
||||
path.dirname(taskMasterPath),
|
||||
'./dist/mcp-server.js'
|
||||
'mcp-server/server.js'
|
||||
);
|
||||
|
||||
// Verify the server file exists
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"outDir": "out",
|
||||
"lib": ["ES2022", "DOM"],
|
||||
"sourceMap": true,
|
||||
"rootDir": "src",
|
||||
"strict": true /* enable all strict type-checking options */,
|
||||
"moduleResolution": "Node",
|
||||
"esModuleInterop": true,
|
||||
@@ -19,11 +20,8 @@
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@/components/*": ["./src/components/*"],
|
||||
"@/lib/*": ["./src/lib/*"],
|
||||
"@tm/core": ["../../packages/tm-core/src/index.ts"],
|
||||
"@tm/core/*": ["../../packages/tm-core/src/*"]
|
||||
"@/lib/*": ["./src/lib/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", ".vscode-test", "out", "dist"]
|
||||
}
|
||||
|
||||
@@ -1,231 +0,0 @@
|
||||
# TODO: Move to apps/docs inside our documentation website
|
||||
|
||||
# Claude Code Integration Guide
|
||||
|
||||
This guide covers how to use Task Master with Claude Code AI SDK integration for enhanced AI-powered development workflows.
|
||||
|
||||
## Overview
|
||||
|
||||
Claude Code integration allows Task Master to leverage the Claude Code CLI for AI operations without requiring direct API keys. The integration uses OAuth tokens managed by the Claude Code CLI itself.
|
||||
|
||||
## Authentication Setup
|
||||
|
||||
The Claude Code provider uses token authentication managed by the Claude Code CLI.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. **Install Claude Code CLI** (if not already installed):
|
||||
|
||||
```bash
|
||||
# Installation method depends on your system
|
||||
# Follow Claude Code documentation for installation
|
||||
```
|
||||
|
||||
2. **Set up OAuth token** using Claude Code CLI:
|
||||
|
||||
```bash
|
||||
claude setup-token
|
||||
```
|
||||
|
||||
This command will:
|
||||
- Guide you through OAuth authentication
|
||||
- Store the token securely for CLI usage
|
||||
- Enable Task Master to use Claude Code without manual API key configuration
|
||||
|
||||
### Authentication Priority
|
||||
|
||||
Task Master will attempt authentication in this order:
|
||||
|
||||
1. **Environment Variable** (optional): `CLAUDE_CODE_OAUTH_TOKEN`
|
||||
- Useful for CI/CD environments or when you want to override the default token
|
||||
- Not required if you've set up the CLI token
|
||||
|
||||
2. **Claude Code CLI Token** (recommended): Token managed by `claude setup-token`
|
||||
- Automatically used when available
|
||||
- Most convenient for local development
|
||||
|
||||
3. **Fallback**: Error if neither is available
|
||||
|
||||
## Configuration
|
||||
|
||||
### Basic Configuration
|
||||
|
||||
Add Claude Code to your Task Master configuration:
|
||||
|
||||
```javascript
|
||||
// In your .taskmaster/config.json or via task-master models command
|
||||
{
|
||||
"models": {
|
||||
"main": "claude-code:sonnet", // Use Claude Code with Sonnet
|
||||
"research": "perplexity-llama-3.1-sonar-large-128k-online",
|
||||
"fallback": "claude-code:opus" // Use Claude Code with Opus as fallback
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Supported Models
|
||||
|
||||
- `claude-code:sonnet` - Claude 3.5 Sonnet via Claude Code CLI
|
||||
- `claude-code:opus` - Claude 3 Opus via Claude Code CLI
|
||||
|
||||
### Environment Variables (Optional)
|
||||
|
||||
While not required, you can optionally set:
|
||||
|
||||
```bash
|
||||
export CLAUDE_CODE_OAUTH_TOKEN="your_oauth_token_here"
|
||||
```
|
||||
|
||||
This is only needed in specific scenarios like:
|
||||
|
||||
- CI/CD pipelines
|
||||
- Docker containers
|
||||
- When you want to use a different token than the CLI default
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Task Operations
|
||||
|
||||
```bash
|
||||
# Use Claude Code for task operations
|
||||
task-master add-task --prompt="Implement user authentication system" --research
|
||||
task-master expand --id=1 --research
|
||||
task-master update-task --id=1.1 --prompt="Add JWT token validation"
|
||||
```
|
||||
|
||||
### Model Configuration Commands
|
||||
|
||||
```bash
|
||||
# Set Claude Code as main model
|
||||
task-master models --set-main claude-code:sonnet
|
||||
|
||||
# Use interactive setup
|
||||
task-master models --setup
|
||||
# Then select "claude-code" from the provider list
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### 1. "Claude Code CLI not available" Error
|
||||
|
||||
**Problem**: Task Master cannot connect to Claude Code CLI.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
- Ensure Claude Code CLI is installed and in your PATH
|
||||
- Run `claude setup-token` to configure authentication
|
||||
- Verify Claude Code CLI works: `claude --help`
|
||||
|
||||
#### 2. Authentication Failures
|
||||
|
||||
**Problem**: Token authentication is failing.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
- Re-run `claude setup-token` to refresh your OAuth token
|
||||
- Check if your token has expired
|
||||
- Verify Claude Code CLI can authenticate: try a simple `claude` command
|
||||
|
||||
#### 3. Model Not Available
|
||||
|
||||
**Problem**: Specified Claude Code model is not supported.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
- Use supported models: `sonnet` or `opus`
|
||||
- Check model availability: `task-master models --list`
|
||||
- Verify your Claude Code CLI has access to the requested model
|
||||
|
||||
### Debug Steps
|
||||
|
||||
1. **Test Claude Code CLI directly**:
|
||||
|
||||
```bash
|
||||
claude --help
|
||||
# Should show help without errors
|
||||
```
|
||||
|
||||
2. **Test authentication**:
|
||||
|
||||
```bash
|
||||
claude setup-token --verify
|
||||
# Should confirm token is valid
|
||||
```
|
||||
|
||||
3. **Test Task Master integration**:
|
||||
|
||||
```bash
|
||||
task-master models --test claude-code:sonnet
|
||||
# Should successfully connect and test the model
|
||||
```
|
||||
|
||||
4. **Check logs**:
|
||||
- Task Master logs will show detailed error messages
|
||||
- Use `--verbose` flag for more detailed output
|
||||
|
||||
### Environment-Specific Configuration
|
||||
|
||||
#### Docker/Containers
|
||||
|
||||
When running in Docker, you'll need to:
|
||||
|
||||
1. Install Claude Code CLI in your container
|
||||
2. Set up authentication via environment variable:
|
||||
|
||||
```dockerfile
|
||||
ENV CLAUDE_CODE_OAUTH_TOKEN="your_token_here"
|
||||
```
|
||||
|
||||
#### CI/CD Pipelines
|
||||
|
||||
For automated environments:
|
||||
|
||||
1. Set up a service account token or use environment variables
|
||||
2. Ensure Claude Code CLI is available in the pipeline environment
|
||||
3. Configure authentication before running Task Master commands
|
||||
|
||||
## Integration with AI SDK
|
||||
|
||||
Task Master's Claude Code integration uses the official `ai-sdk-provider-claude-code` package, providing:
|
||||
|
||||
- **Streaming Support**: Real-time token streaming for interactive experiences
|
||||
- **Full AI SDK Compatibility**: Works with generateText, streamText, and other AI SDK functions
|
||||
- **Automatic Error Handling**: Graceful degradation when Claude Code is unavailable
|
||||
- **Type Safety**: Full TypeScript support with proper type definitions
|
||||
|
||||
### Example AI SDK Usage
|
||||
|
||||
```javascript
|
||||
import { generateText } from 'ai';
|
||||
import { ClaudeCodeProvider } from './src/ai-providers/claude-code.js';
|
||||
|
||||
const provider = new ClaudeCodeProvider();
|
||||
const client = provider.getClient();
|
||||
|
||||
const result = await generateText({
|
||||
model: client('sonnet'),
|
||||
messages: [
|
||||
{ role: 'user', content: 'Hello Claude!' }
|
||||
]
|
||||
});
|
||||
|
||||
console.log(result.text);
|
||||
```
|
||||
|
||||
## Security Notes
|
||||
|
||||
- OAuth tokens are managed securely by Claude Code CLI
|
||||
- No API keys need to be stored in your project files
|
||||
- Tokens are automatically refreshed by the Claude Code CLI
|
||||
- Environment variables should only be used in secure environments
|
||||
|
||||
## Getting Help
|
||||
|
||||
If you encounter issues:
|
||||
|
||||
1. Check the Claude Code CLI documentation
|
||||
2. Verify your authentication setup with `claude setup-token --verify`
|
||||
3. Review Task Master logs for detailed error messages
|
||||
4. Open an issue with both Task Master and Claude Code version information
|
||||
@@ -235,60 +235,6 @@ node scripts/init.js
|
||||
- "MCP provider requires session context" → Ensure running in MCP environment
|
||||
- See the [MCP Provider Guide](./mcp-provider-guide.md) for detailed troubleshooting
|
||||
|
||||
### MCP Timeout Configuration
|
||||
|
||||
Long-running AI operations in taskmaster-ai can exceed the default 60-second MCP timeout. Operations like `parse_prd`, `expand_task`, `research`, and `analyze_project_complexity` may take 2-5 minutes to complete.
|
||||
|
||||
#### Adding Timeout Configuration
|
||||
|
||||
Add a `timeout` parameter to your MCP configuration to extend the timeout limit. The timeout configuration works identically across MCP clients including Cursor, Windsurf, and RooCode:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"task-master-ai": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "--package=task-master-ai", "task-master-ai"],
|
||||
"timeout": 300,
|
||||
"env": {
|
||||
"ANTHROPIC_API_KEY": "your-anthropic-api-key"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Configuration Details:**
|
||||
- **`timeout: 300`** - Sets timeout to 300 seconds (5 minutes)
|
||||
- **Value range**: 1-3600 seconds (1 second to 1 hour)
|
||||
- **Recommended**: 300 seconds provides sufficient time for most AI operations
|
||||
- **Format**: Integer value in seconds (not milliseconds)
|
||||
|
||||
#### Automatic Setup
|
||||
|
||||
When adding taskmaster rules for supported editors, the timeout configuration is automatically included:
|
||||
|
||||
```bash
|
||||
# Automatically includes timeout configuration
|
||||
task-master rules add cursor
|
||||
task-master rules add roo
|
||||
task-master rules add windsurf
|
||||
task-master rules add vscode
|
||||
```
|
||||
|
||||
#### Troubleshooting Timeouts
|
||||
|
||||
If you're still experiencing timeout errors:
|
||||
|
||||
1. **Verify configuration**: Check that `timeout: 300` is present in your MCP config
|
||||
2. **Restart editor**: Restart your editor after making configuration changes
|
||||
3. **Increase timeout**: For very complex operations, try `timeout: 600` (10 minutes)
|
||||
4. **Check API keys**: Ensure required API keys are properly configured
|
||||
|
||||
**Expected behavior:**
|
||||
- **Before fix**: Operations fail after 60 seconds with `MCP request timed out after 60000ms`
|
||||
- **After fix**: Operations complete successfully within the configured timeout limit
|
||||
|
||||
### Google Vertex AI Configuration
|
||||
|
||||
Google Vertex AI is Google Cloud's enterprise AI platform and requires specific configuration:
|
||||
|
||||
@@ -451,8 +451,8 @@ When using Task Master in VS Code with MCP support:
|
||||
{
|
||||
"servers": {
|
||||
"task-master-dev": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "task-master-ai"],
|
||||
"command": "node",
|
||||
"args": ["mcp-server/server.js"],
|
||||
"cwd": "/path/to/your/task-master-project",
|
||||
"env": {
|
||||
"NODE_ENV": "development",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Available Models as of September 23, 2025
|
||||
# Available Models as of August 12, 2025
|
||||
|
||||
## Main Models
|
||||
|
||||
@@ -13,10 +13,6 @@
|
||||
| mcp | mcp-sampling | — | 0 | 0 |
|
||||
| gemini-cli | gemini-2.5-pro | 0.72 | 0 | 0 |
|
||||
| gemini-cli | gemini-2.5-flash | 0.71 | 0 | 0 |
|
||||
| grok-cli | grok-4-latest | 0.7 | 0 | 0 |
|
||||
| grok-cli | grok-3-latest | 0.65 | 0 | 0 |
|
||||
| grok-cli | grok-3-fast | 0.6 | 0 | 0 |
|
||||
| grok-cli | grok-3-mini-fast | 0.55 | 0 | 0 |
|
||||
| openai | gpt-4o | 0.332 | 2.5 | 10 |
|
||||
| openai | o1 | 0.489 | 15 | 60 |
|
||||
| openai | o3 | 0.5 | 2 | 8 |
|
||||
@@ -103,10 +99,6 @@
|
||||
| mcp | mcp-sampling | — | 0 | 0 |
|
||||
| gemini-cli | gemini-2.5-pro | 0.72 | 0 | 0 |
|
||||
| gemini-cli | gemini-2.5-flash | 0.71 | 0 | 0 |
|
||||
| grok-cli | grok-4-latest | 0.7 | 0 | 0 |
|
||||
| grok-cli | grok-3-latest | 0.65 | 0 | 0 |
|
||||
| grok-cli | grok-3-fast | 0.6 | 0 | 0 |
|
||||
| grok-cli | grok-3-mini-fast | 0.55 | 0 | 0 |
|
||||
| openai | gpt-4o-search-preview | 0.33 | 2.5 | 10 |
|
||||
| openai | gpt-4o-mini-search-preview | 0.3 | 0.15 | 0.6 |
|
||||
| xai | grok-3 | — | 3 | 15 |
|
||||
@@ -119,7 +111,7 @@
|
||||
| groq | deepseek-r1-distill-llama-70b | 0.52 | 0.75 | 0.99 |
|
||||
| perplexity | sonar-pro | — | 3 | 15 |
|
||||
| perplexity | sonar | — | 1 | 1 |
|
||||
| perplexity | sonar-deep-research | 0.211 | 2 | 8 |
|
||||
| perplexity | deep-research | 0.211 | 2 | 8 |
|
||||
| perplexity | sonar-reasoning-pro | 0.211 | 2 | 8 |
|
||||
| perplexity | sonar-reasoning | 0.211 | 1 | 5 |
|
||||
| bedrock | us.anthropic.claude-3-opus-20240229-v1:0 | 0.725 | 15 | 75 |
|
||||
@@ -143,10 +135,6 @@
|
||||
| mcp | mcp-sampling | — | 0 | 0 |
|
||||
| gemini-cli | gemini-2.5-pro | 0.72 | 0 | 0 |
|
||||
| gemini-cli | gemini-2.5-flash | 0.71 | 0 | 0 |
|
||||
| grok-cli | grok-4-latest | 0.7 | 0 | 0 |
|
||||
| grok-cli | grok-3-latest | 0.65 | 0 | 0 |
|
||||
| grok-cli | grok-3-fast | 0.6 | 0 | 0 |
|
||||
| grok-cli | grok-3-mini-fast | 0.55 | 0 | 0 |
|
||||
| openai | gpt-4o | 0.332 | 2.5 | 10 |
|
||||
| openai | o3 | 0.5 | 2 | 8 |
|
||||
| openai | o4-mini | 0.45 | 1.1 | 4.4 |
|
||||
|
||||
@@ -23,7 +23,7 @@ npm i -g task-master-ai
|
||||
"mcpServers": {
|
||||
"taskmaster-ai": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "task-master-ai"],
|
||||
"args": ["-y", "--package=task-master-ai", "task-master-ai"],
|
||||
"env": {
|
||||
"ANTHROPIC_API_KEY": "YOUR_ANTHROPIC_API_KEY_HERE",
|
||||
"PERPLEXITY_API_KEY": "YOUR_PERPLEXITY_API_KEY_HERE",
|
||||
@@ -145,7 +145,7 @@ You can also set up the MCP server in Cursor settings:
|
||||
4. Configure with the following details:
|
||||
- Name: "Task Master"
|
||||
- Type: "Command"
|
||||
- Command: "npx -y task-master-ai"
|
||||
- Command: "npx -y --package=task-master-ai task-master-ai"
|
||||
5. Save the settings
|
||||
|
||||
Once configured, you can interact with Task Master's task management commands directly through Cursor's interface, providing a more integrated experience.
|
||||
|
||||
@@ -17,7 +17,7 @@ Add the following configuration to the user's MCP settings file (`.cursor/mcp.js
|
||||
"mcpServers": {
|
||||
"taskmaster-ai": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "task-master-ai"],
|
||||
"args": ["-y", "--package=task-master-ai", "task-master-ai"],
|
||||
"env": {
|
||||
"ANTHROPIC_API_KEY": "user_will_add_their_key_here",
|
||||
"PERPLEXITY_API_KEY": "user_will_add_their_key_here",
|
||||
|
||||
6472
package-lock.json
generated
6472
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
64
package.json
64
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "task-master-ai",
|
||||
"version": "0.27.3",
|
||||
"version": "0.26.0",
|
||||
"description": "A task management system for ambitious AI-driven development that doesn't overwhelm and confuse Cursor.",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
@@ -12,12 +12,12 @@
|
||||
"workspaces": ["apps/*", "packages/*", "."],
|
||||
"scripts": {
|
||||
"build": "npm run build:build-config && cross-env NODE_ENV=production tsdown",
|
||||
"dev": "tsdown --watch",
|
||||
"dev": "tsdown --watch='packages/*/src/**/*' --watch='apps/cli/src/**/*' --watch='bin/**/*' --watch='mcp-server/**/*'",
|
||||
"turbo:dev": "turbo dev",
|
||||
"turbo:build": "turbo build",
|
||||
"turbo:typecheck": "turbo typecheck",
|
||||
"build:build-config": "npm run build -w @tm/build-config",
|
||||
"test": "cross-env NODE_ENV=test node --experimental-vm-modules node_modules/.bin/jest",
|
||||
"test": "node --experimental-vm-modules node_modules/.bin/jest",
|
||||
"test:unit": "node --experimental-vm-modules node_modules/.bin/jest --testPathPattern=unit",
|
||||
"test:integration": "node --experimental-vm-modules node_modules/.bin/jest --testPathPattern=integration",
|
||||
"test:fails": "node --experimental-vm-modules node_modules/.bin/jest --onlyFailures",
|
||||
@@ -33,9 +33,7 @@
|
||||
"inspector": "npx @modelcontextprotocol/inspector node dist/mcp-server.js",
|
||||
"mcp-server": "node dist/mcp-server.js",
|
||||
"format-check": "biome format .",
|
||||
"format": "biome format . --write",
|
||||
"deps:check": "manypkg check || echo 'Note: Workspace package version warnings are expected for internal @tm/* packages'",
|
||||
"deps:fix": "manypkg fix"
|
||||
"format": "biome format . --write"
|
||||
},
|
||||
"keywords": [
|
||||
"claude",
|
||||
@@ -52,26 +50,23 @@
|
||||
"author": "Eyal Toledano",
|
||||
"license": "MIT WITH Commons-Clause",
|
||||
"dependencies": {
|
||||
"@ai-sdk/amazon-bedrock": "^3.0.23",
|
||||
"@ai-sdk/anthropic": "^2.0.18",
|
||||
"@ai-sdk/azure": "^2.0.34",
|
||||
"@ai-sdk/google": "^2.0.16",
|
||||
"@ai-sdk/google-vertex": "^3.0.29",
|
||||
"@ai-sdk/groq": "^2.0.21",
|
||||
"@ai-sdk/mistral": "^2.0.16",
|
||||
"@ai-sdk/openai": "^2.0.34",
|
||||
"@ai-sdk/perplexity": "^2.0.10",
|
||||
"@ai-sdk/provider": "^2.0.0",
|
||||
"@ai-sdk/provider-utils": "^3.0.10",
|
||||
"@ai-sdk/xai": "^2.0.22",
|
||||
"@aws-sdk/credential-providers": "^3.895.0",
|
||||
"@ai-sdk/amazon-bedrock": "^2.2.9",
|
||||
"@ai-sdk/anthropic": "^1.2.10",
|
||||
"@ai-sdk/azure": "^1.3.17",
|
||||
"@ai-sdk/google": "^1.2.13",
|
||||
"@ai-sdk/google-vertex": "^2.2.23",
|
||||
"@ai-sdk/groq": "^1.2.9",
|
||||
"@ai-sdk/mistral": "^1.2.7",
|
||||
"@ai-sdk/openai": "^1.3.20",
|
||||
"@ai-sdk/perplexity": "^1.1.7",
|
||||
"@ai-sdk/xai": "^1.2.15",
|
||||
"@anthropic-ai/sdk": "^0.39.0",
|
||||
"@aws-sdk/credential-providers": "^3.817.0",
|
||||
"@inquirer/search": "^3.0.15",
|
||||
"@openrouter/ai-sdk-provider": "^1.2.0",
|
||||
"@openrouter/ai-sdk-provider": "^0.4.5",
|
||||
"@streamparser/json": "^0.0.22",
|
||||
"@supabase/supabase-js": "^2.57.4",
|
||||
"ai": "^5.0.51",
|
||||
"ai-sdk-provider-claude-code": "^1.1.4",
|
||||
"ai-sdk-provider-gemini-cli": "^1.1.1",
|
||||
"@tm/cli": "*",
|
||||
"ai": "^4.3.10",
|
||||
"ajv": "^8.17.1",
|
||||
"ajv-formats": "^3.0.1",
|
||||
"boxen": "^8.0.1",
|
||||
@@ -79,9 +74,9 @@
|
||||
"cli-highlight": "^2.1.11",
|
||||
"cli-progress": "^3.12.0",
|
||||
"cli-table3": "^0.6.5",
|
||||
"commander": "^12.1.0",
|
||||
"commander": "^11.1.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.6.1",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.21.2",
|
||||
"fastmcp": "^3.5.0",
|
||||
"figlet": "^1.8.0",
|
||||
@@ -96,14 +91,17 @@
|
||||
"lru-cache": "^10.2.0",
|
||||
"marked": "^15.0.12",
|
||||
"marked-terminal": "^7.3.0",
|
||||
"ollama-ai-provider-v2": "^1.3.1",
|
||||
"ollama-ai-provider": "^1.2.0",
|
||||
"openai": "^4.89.0",
|
||||
"ora": "^8.2.0",
|
||||
"uuid": "^11.1.0",
|
||||
"zod": "^4.1.11"
|
||||
"zod": "^3.23.8",
|
||||
"zod-to-json-schema": "^3.24.5"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@anthropic-ai/claude-code": "^1.0.88",
|
||||
"@biomejs/cli-linux-x64": "^1.9.4"
|
||||
"@biomejs/cli-linux-x64": "^1.9.4",
|
||||
"ai-sdk-provider-gemini-cli": "^0.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
@@ -126,13 +124,11 @@
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@changesets/changelog-github": "^0.5.1",
|
||||
"@changesets/cli": "^2.28.1",
|
||||
"@manypkg/cli": "^0.25.1",
|
||||
"@tm/ai-sdk-provider-grok-cli": "*",
|
||||
"@tm/cli": "*",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/marked-terminal": "^6.1.1",
|
||||
"concurrently": "^9.2.1",
|
||||
"cross-env": "^10.0.0",
|
||||
"dotenv-mono": "^1.5.1",
|
||||
"execa": "^8.0.1",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-node": "^29.7.0",
|
||||
@@ -141,8 +137,8 @@
|
||||
"supertest": "^7.1.0",
|
||||
"ts-jest": "^29.4.2",
|
||||
"tsdown": "^0.15.2",
|
||||
"tsx": "^4.20.4",
|
||||
"turbo": "2.5.6",
|
||||
"tsx": "^4.16.2",
|
||||
"turbo": "^2.5.6",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
# AI SDK Provider for Grok CLI
|
||||
|
||||
A provider for the [AI SDK](https://sdk.vercel.ai) that integrates with [Grok CLI](https://docs.x.ai/api) for accessing xAI's Grok language models.
|
||||
|
||||
## Features
|
||||
|
||||
- ✅ **AI SDK v5 Compatible** - Full support for the latest AI SDK interfaces
|
||||
- ✅ **Streaming & Non-streaming** - Both generation modes supported
|
||||
- ✅ **Error Handling** - Comprehensive error handling with retry logic
|
||||
- ✅ **Type Safety** - Full TypeScript support with proper type definitions
|
||||
- ✅ **JSON Mode** - Automatic JSON extraction from responses
|
||||
- ✅ **Abort Signals** - Proper cancellation support
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @tm/ai-sdk-provider-grok-cli
|
||||
# or
|
||||
yarn add @tm/ai-sdk-provider-grok-cli
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. Install the Grok CLI:
|
||||
|
||||
```bash
|
||||
npm install -g grok-cli
|
||||
# or follow xAI's installation instructions
|
||||
```
|
||||
|
||||
2. Set up authentication:
|
||||
|
||||
```bash
|
||||
export GROK_CLI_API_KEY="your-api-key"
|
||||
# or configure via grok CLI: grok config set api-key your-key
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```typescript
|
||||
import { grokCli } from '@tm/ai-sdk-provider-grok-cli';
|
||||
import { generateText } from 'ai';
|
||||
|
||||
const result = await generateText({
|
||||
model: grokCli('grok-3-latest'),
|
||||
prompt: 'Write a haiku about TypeScript'
|
||||
});
|
||||
|
||||
console.log(result.text);
|
||||
```
|
||||
|
||||
### Streaming
|
||||
|
||||
```typescript
|
||||
import { grokCli } from '@tm/ai-sdk-provider-grok-cli';
|
||||
import { streamText } from 'ai';
|
||||
|
||||
const { textStream } = await streamText({
|
||||
model: grokCli('grok-4-latest'),
|
||||
prompt: 'Explain quantum computing'
|
||||
});
|
||||
|
||||
for await (const delta of textStream) {
|
||||
process.stdout.write(delta);
|
||||
}
|
||||
```
|
||||
|
||||
### JSON Mode
|
||||
|
||||
```typescript
|
||||
import { grokCli } from '@tm/ai-sdk-provider-grok-cli';
|
||||
import { generateObject } from 'ai';
|
||||
import { z } from 'zod';
|
||||
|
||||
const result = await generateObject({
|
||||
model: grokCli('grok-3-latest'),
|
||||
schema: z.object({
|
||||
name: z.string(),
|
||||
age: z.number(),
|
||||
hobbies: z.array(z.string())
|
||||
}),
|
||||
prompt: 'Generate a person profile'
|
||||
});
|
||||
|
||||
console.log(result.object);
|
||||
```
|
||||
|
||||
## Supported Models
|
||||
|
||||
- `grok-3-latest` - Grok 3 (latest version)
|
||||
- `grok-4-latest` - Grok 4 (latest version)
|
||||
- `grok-4` - Grok 4 (stable)
|
||||
- Custom model strings supported
|
||||
|
||||
## Configuration
|
||||
|
||||
### Provider Settings
|
||||
|
||||
```typescript
|
||||
import { createGrokCli } from '@tm/ai-sdk-provider-grok-cli';
|
||||
|
||||
const grok = createGrokCli({
|
||||
apiKey: 'your-api-key', // Optional if set via env/CLI
|
||||
timeout: 120000, // 2 minutes default
|
||||
workingDirectory: '/path/to/project', // Optional
|
||||
baseURL: 'https://api.x.ai' // Optional
|
||||
});
|
||||
```
|
||||
|
||||
### Model Settings
|
||||
|
||||
```typescript
|
||||
const model = grok('grok-4-latest', {
|
||||
timeout: 300000, // 5 minutes for grok-4
|
||||
// Other CLI-specific settings
|
||||
});
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
The provider includes comprehensive error handling:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
isAuthenticationError,
|
||||
isTimeoutError,
|
||||
isInstallationError
|
||||
} from '@tm/ai-sdk-provider-grok-cli';
|
||||
|
||||
try {
|
||||
const result = await generateText({
|
||||
model: grokCli('grok-4-latest'),
|
||||
prompt: 'Hello!'
|
||||
});
|
||||
} catch (error) {
|
||||
if (isAuthenticationError(error)) {
|
||||
console.error('Authentication failed:', error.message);
|
||||
} else if (isTimeoutError(error)) {
|
||||
console.error('Request timed out:', error.message);
|
||||
} else if (isInstallationError(error)) {
|
||||
console.error('Grok CLI not installed or not found in PATH');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Start development mode (keep running during development)
|
||||
npm run dev
|
||||
|
||||
# Type check
|
||||
npm run typecheck
|
||||
|
||||
# Run tests (requires build first)
|
||||
NODE_ENV=production npm run build
|
||||
npm test
|
||||
```
|
||||
|
||||
**Important**: Always run `npm run dev` and keep it running during development. This ensures proper compilation and hot-reloading of TypeScript files.
|
||||
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"name": "@tm/ai-sdk-provider-grok-cli",
|
||||
"private": true,
|
||||
"description": "AI SDK provider for Grok CLI integration",
|
||||
"type": "module",
|
||||
"types": "./src/index.ts",
|
||||
"main": "./dist/index.js",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:ui": "vitest --ui",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "^2.0.0",
|
||||
"@ai-sdk/provider-utils": "^3.0.10",
|
||||
"jsonc-parser": "^3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.18.6",
|
||||
"typescript": "^5.9.2",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"keywords": ["ai", "grok", "x.ai", "cli", "language-model", "provider"],
|
||||
"files": ["dist/**/*", "README.md"],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
/**
|
||||
* Tests for error handling utilities
|
||||
*/
|
||||
|
||||
import { APICallError, LoadAPIKeyError } from '@ai-sdk/provider';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
createAPICallError,
|
||||
createAuthenticationError,
|
||||
createInstallationError,
|
||||
createTimeoutError,
|
||||
getErrorMetadata,
|
||||
isAuthenticationError,
|
||||
isInstallationError,
|
||||
isTimeoutError
|
||||
} from './errors.js';
|
||||
|
||||
describe('createAPICallError', () => {
|
||||
it('should create APICallError with metadata', () => {
|
||||
const error = createAPICallError({
|
||||
message: 'Test error',
|
||||
code: 'TEST_ERROR',
|
||||
exitCode: 1,
|
||||
stderr: 'Error output',
|
||||
stdout: 'Success output',
|
||||
promptExcerpt: 'Test prompt',
|
||||
isRetryable: true
|
||||
});
|
||||
|
||||
expect(error).toBeInstanceOf(APICallError);
|
||||
expect(error.message).toBe('Test error');
|
||||
expect(error.isRetryable).toBe(true);
|
||||
expect(error.url).toBe('grok-cli://command');
|
||||
expect(error.data).toEqual({
|
||||
code: 'TEST_ERROR',
|
||||
exitCode: 1,
|
||||
stderr: 'Error output',
|
||||
stdout: 'Success output',
|
||||
promptExcerpt: 'Test prompt'
|
||||
});
|
||||
});
|
||||
|
||||
it('should create APICallError with minimal parameters', () => {
|
||||
const error = createAPICallError({
|
||||
message: 'Simple error'
|
||||
});
|
||||
|
||||
expect(error).toBeInstanceOf(APICallError);
|
||||
expect(error.message).toBe('Simple error');
|
||||
expect(error.isRetryable).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createAuthenticationError', () => {
|
||||
it('should create LoadAPIKeyError with custom message', () => {
|
||||
const error = createAuthenticationError({
|
||||
message: 'Custom auth error'
|
||||
});
|
||||
|
||||
expect(error).toBeInstanceOf(LoadAPIKeyError);
|
||||
expect(error.message).toBe('Custom auth error');
|
||||
});
|
||||
|
||||
it('should create LoadAPIKeyError with default message', () => {
|
||||
const error = createAuthenticationError({});
|
||||
|
||||
expect(error).toBeInstanceOf(LoadAPIKeyError);
|
||||
expect(error.message).toContain('Authentication failed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createTimeoutError', () => {
|
||||
it('should create APICallError for timeout', () => {
|
||||
const error = createTimeoutError({
|
||||
message: 'Operation timed out',
|
||||
timeoutMs: 5000,
|
||||
promptExcerpt: 'Test prompt'
|
||||
});
|
||||
|
||||
expect(error).toBeInstanceOf(APICallError);
|
||||
expect(error.message).toBe('Operation timed out');
|
||||
expect(error.isRetryable).toBe(true);
|
||||
expect(error.data).toEqual({
|
||||
code: 'TIMEOUT',
|
||||
promptExcerpt: 'Test prompt',
|
||||
timeoutMs: 5000
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createInstallationError', () => {
|
||||
it('should create APICallError for installation issues', () => {
|
||||
const error = createInstallationError({
|
||||
message: 'CLI not found'
|
||||
});
|
||||
|
||||
expect(error).toBeInstanceOf(APICallError);
|
||||
expect(error.message).toBe('CLI not found');
|
||||
expect(error.isRetryable).toBe(false);
|
||||
expect(error.url).toBe('grok-cli://installation');
|
||||
});
|
||||
|
||||
it('should create APICallError with default message', () => {
|
||||
const error = createInstallationError({});
|
||||
|
||||
expect(error).toBeInstanceOf(APICallError);
|
||||
expect(error.message).toContain('Grok CLI is not installed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isAuthenticationError', () => {
|
||||
it('should return true for LoadAPIKeyError', () => {
|
||||
const error = new LoadAPIKeyError({ message: 'Auth failed' });
|
||||
expect(isAuthenticationError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for APICallError with 401 exit code', () => {
|
||||
const error = new APICallError({
|
||||
message: 'Unauthorized',
|
||||
data: { exitCode: 401 }
|
||||
});
|
||||
expect(isAuthenticationError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for other errors', () => {
|
||||
const error = new Error('Generic error');
|
||||
expect(isAuthenticationError(error)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isTimeoutError', () => {
|
||||
it('should return true for timeout APICallError', () => {
|
||||
const error = new APICallError({
|
||||
message: 'Timeout',
|
||||
data: { code: 'TIMEOUT' }
|
||||
});
|
||||
expect(isTimeoutError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for other errors', () => {
|
||||
const error = new APICallError({ message: 'Other error' });
|
||||
expect(isTimeoutError(error)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isInstallationError', () => {
|
||||
it('should return true for installation APICallError', () => {
|
||||
const error = new APICallError({
|
||||
message: 'Not installed',
|
||||
url: 'grok-cli://installation'
|
||||
});
|
||||
expect(isInstallationError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for other errors', () => {
|
||||
const error = new APICallError({ message: 'Other error' });
|
||||
expect(isInstallationError(error)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getErrorMetadata', () => {
|
||||
it('should return metadata from APICallError', () => {
|
||||
const metadata = {
|
||||
code: 'TEST_ERROR',
|
||||
exitCode: 1,
|
||||
stderr: 'Error output'
|
||||
};
|
||||
const error = new APICallError({
|
||||
message: 'Test error',
|
||||
data: metadata
|
||||
});
|
||||
|
||||
const result = getErrorMetadata(error);
|
||||
expect(result).toEqual(metadata);
|
||||
});
|
||||
|
||||
it('should return undefined for errors without metadata', () => {
|
||||
const error = new Error('Generic error');
|
||||
const result = getErrorMetadata(error);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined for APICallError without data', () => {
|
||||
const error = new APICallError({ message: 'Test error' });
|
||||
const result = getErrorMetadata(error);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -1,187 +0,0 @@
|
||||
/**
|
||||
* Error handling utilities for Grok CLI provider
|
||||
*/
|
||||
|
||||
import { APICallError, LoadAPIKeyError } from '@ai-sdk/provider';
|
||||
import type { GrokCliErrorMetadata } from './types.js';
|
||||
|
||||
/**
|
||||
* Parameters for creating API call errors
|
||||
*/
|
||||
interface CreateAPICallErrorParams {
|
||||
/** Error message */
|
||||
message: string;
|
||||
/** Error code */
|
||||
code?: string;
|
||||
/** Process exit code */
|
||||
exitCode?: number;
|
||||
/** Standard error output */
|
||||
stderr?: string;
|
||||
/** Standard output */
|
||||
stdout?: string;
|
||||
/** Excerpt of the prompt */
|
||||
promptExcerpt?: string;
|
||||
/** Whether the error is retryable */
|
||||
isRetryable?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for creating authentication errors
|
||||
*/
|
||||
interface CreateAuthenticationErrorParams {
|
||||
/** Error message */
|
||||
message?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for creating timeout errors
|
||||
*/
|
||||
interface CreateTimeoutErrorParams {
|
||||
/** Error message */
|
||||
message: string;
|
||||
/** Excerpt of the prompt */
|
||||
promptExcerpt?: string;
|
||||
/** Timeout in milliseconds */
|
||||
timeoutMs: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for creating installation errors
|
||||
*/
|
||||
interface CreateInstallationErrorParams {
|
||||
/** Error message */
|
||||
message?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an API call error with Grok CLI specific metadata
|
||||
*/
|
||||
export function createAPICallError({
|
||||
message,
|
||||
code,
|
||||
exitCode,
|
||||
stderr,
|
||||
stdout,
|
||||
promptExcerpt,
|
||||
isRetryable = false
|
||||
}: CreateAPICallErrorParams): APICallError {
|
||||
const metadata: GrokCliErrorMetadata = {
|
||||
code,
|
||||
exitCode,
|
||||
stderr,
|
||||
stdout,
|
||||
promptExcerpt
|
||||
};
|
||||
|
||||
return new APICallError({
|
||||
message,
|
||||
isRetryable,
|
||||
url: 'grok-cli://command',
|
||||
requestBodyValues: promptExcerpt ? { prompt: promptExcerpt } : undefined,
|
||||
data: metadata
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an authentication error
|
||||
*/
|
||||
export function createAuthenticationError({
|
||||
message
|
||||
}: CreateAuthenticationErrorParams): LoadAPIKeyError {
|
||||
return new LoadAPIKeyError({
|
||||
message:
|
||||
message ||
|
||||
'Authentication failed. Please ensure Grok CLI is properly configured with API key.'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a timeout error
|
||||
*/
|
||||
export function createTimeoutError({
|
||||
message,
|
||||
promptExcerpt,
|
||||
timeoutMs
|
||||
}: CreateTimeoutErrorParams): APICallError {
|
||||
const metadata: GrokCliErrorMetadata & { timeoutMs: number } = {
|
||||
code: 'TIMEOUT',
|
||||
promptExcerpt,
|
||||
timeoutMs
|
||||
};
|
||||
|
||||
return new APICallError({
|
||||
message,
|
||||
isRetryable: true,
|
||||
url: 'grok-cli://command',
|
||||
requestBodyValues: promptExcerpt ? { prompt: promptExcerpt } : undefined,
|
||||
data: metadata
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a CLI installation error
|
||||
*/
|
||||
export function createInstallationError({
|
||||
message
|
||||
}: CreateInstallationErrorParams): APICallError {
|
||||
return new APICallError({
|
||||
message:
|
||||
message ||
|
||||
'Grok CLI is not installed or not found in PATH. Please install with: npm install -g @vibe-kit/grok-cli',
|
||||
isRetryable: false,
|
||||
url: 'grok-cli://installation',
|
||||
requestBodyValues: undefined
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an error is an authentication error
|
||||
*/
|
||||
export function isAuthenticationError(
|
||||
error: unknown
|
||||
): error is LoadAPIKeyError {
|
||||
if (error instanceof LoadAPIKeyError) return true;
|
||||
if (error instanceof APICallError) {
|
||||
const metadata = error.data as GrokCliErrorMetadata | undefined;
|
||||
if (!metadata) return false;
|
||||
return (
|
||||
metadata.exitCode === 401 ||
|
||||
metadata.code === 'AUTHENTICATION_ERROR' ||
|
||||
metadata.code === 'UNAUTHORIZED'
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an error is a timeout error
|
||||
*/
|
||||
export function isTimeoutError(error: unknown): error is APICallError {
|
||||
if (
|
||||
error instanceof APICallError &&
|
||||
(error.data as GrokCliErrorMetadata)?.code === 'TIMEOUT'
|
||||
)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an error is an installation error
|
||||
*/
|
||||
export function isInstallationError(error: unknown): error is APICallError {
|
||||
if (error instanceof APICallError && error.url === 'grok-cli://installation')
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get error metadata from an error
|
||||
*/
|
||||
export function getErrorMetadata(
|
||||
error: unknown
|
||||
): GrokCliErrorMetadata | undefined {
|
||||
if (error instanceof APICallError && error.data) {
|
||||
return error.data as GrokCliErrorMetadata;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -1,520 +0,0 @@
|
||||
/**
|
||||
* Grok CLI Language Model implementation for AI SDK v5
|
||||
*/
|
||||
|
||||
import { spawn } from 'child_process';
|
||||
import { promises as fs } from 'fs';
|
||||
import { homedir } from 'os';
|
||||
import { join } from 'path';
|
||||
import type {
|
||||
LanguageModelV2,
|
||||
LanguageModelV2CallOptions,
|
||||
LanguageModelV2CallWarning
|
||||
} from '@ai-sdk/provider';
|
||||
import { NoSuchModelError } from '@ai-sdk/provider';
|
||||
import { generateId } from '@ai-sdk/provider-utils';
|
||||
|
||||
import {
|
||||
createAPICallError,
|
||||
createAuthenticationError,
|
||||
createInstallationError,
|
||||
createTimeoutError
|
||||
} from './errors.js';
|
||||
import { extractJson } from './json-extractor.js';
|
||||
import {
|
||||
convertFromGrokCliResponse,
|
||||
createPromptFromMessages,
|
||||
escapeShellArg
|
||||
} from './message-converter.js';
|
||||
import type {
|
||||
GrokCliLanguageModelOptions,
|
||||
GrokCliModelId,
|
||||
GrokCliSettings
|
||||
} from './types.js';
|
||||
|
||||
/**
|
||||
* Grok CLI Language Model implementation for AI SDK v5
|
||||
*/
|
||||
export class GrokCliLanguageModel implements LanguageModelV2 {
|
||||
readonly specificationVersion = 'v2' as const;
|
||||
readonly defaultObjectGenerationMode = 'json' as const;
|
||||
readonly supportsImageUrls = false;
|
||||
readonly supportsStructuredOutputs = false;
|
||||
readonly supportedUrls: Record<string, RegExp[]> = {};
|
||||
|
||||
readonly modelId: GrokCliModelId;
|
||||
readonly settings: GrokCliSettings;
|
||||
|
||||
constructor(options: GrokCliLanguageModelOptions) {
|
||||
this.modelId = options.id;
|
||||
this.settings = options.settings ?? {};
|
||||
|
||||
// Validate model ID format
|
||||
if (
|
||||
!this.modelId ||
|
||||
typeof this.modelId !== 'string' ||
|
||||
this.modelId.trim() === ''
|
||||
) {
|
||||
throw new NoSuchModelError({
|
||||
modelId: this.modelId,
|
||||
modelType: 'languageModel'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get provider(): string {
|
||||
return 'grok-cli';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Grok CLI is installed and available
|
||||
*/
|
||||
private async checkGrokCliInstallation(): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn('grok', ['--version'], {
|
||||
stdio: 'pipe'
|
||||
});
|
||||
|
||||
child.on('error', () => resolve(false));
|
||||
child.on('exit', (code) => resolve(code === 0));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get API key from settings or environment
|
||||
*/
|
||||
private async getApiKey(): Promise<string | null> {
|
||||
// Check settings first
|
||||
if (this.settings.apiKey) {
|
||||
return this.settings.apiKey;
|
||||
}
|
||||
|
||||
// Check environment variable
|
||||
if (process.env.GROK_CLI_API_KEY) {
|
||||
return process.env.GROK_CLI_API_KEY;
|
||||
}
|
||||
|
||||
// Check grok-cli config file
|
||||
try {
|
||||
const configPath = join(homedir(), '.grok', 'user-settings.json');
|
||||
const configContent = await fs.readFile(configPath, 'utf8');
|
||||
const config = JSON.parse(configContent);
|
||||
return config.apiKey || null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute Grok CLI command
|
||||
*/
|
||||
private async executeGrokCli(
|
||||
args: string[],
|
||||
options: { timeout?: number; apiKey?: string } = {}
|
||||
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
|
||||
// Default timeout based on model type
|
||||
let defaultTimeout = 120000; // 2 minutes default
|
||||
if (this.modelId.includes('grok-4')) {
|
||||
defaultTimeout = 600000; // 10 minutes for grok-4 models (they seem to hang during setup)
|
||||
}
|
||||
|
||||
const timeout = options.timeout ?? this.settings.timeout ?? defaultTimeout;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn('grok', args, {
|
||||
stdio: 'pipe',
|
||||
cwd: this.settings.workingDirectory || process.cwd(),
|
||||
env:
|
||||
options.apiKey === undefined
|
||||
? process.env
|
||||
: { ...process.env, GROK_CLI_API_KEY: options.apiKey }
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
let timeoutId: NodeJS.Timeout | undefined;
|
||||
|
||||
// Set up timeout
|
||||
if (timeout > 0) {
|
||||
timeoutId = setTimeout(() => {
|
||||
child.kill('SIGTERM');
|
||||
reject(
|
||||
createTimeoutError({
|
||||
message: `Grok CLI command timed out after ${timeout}ms`,
|
||||
timeoutMs: timeout,
|
||||
promptExcerpt: args.join(' ').substring(0, 200)
|
||||
})
|
||||
);
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
child.stdout?.on('data', (data) => {
|
||||
const chunk = data.toString();
|
||||
stdout += chunk;
|
||||
});
|
||||
|
||||
child.stderr?.on('data', (data) => {
|
||||
const chunk = data.toString();
|
||||
stderr += chunk;
|
||||
});
|
||||
|
||||
child.on('error', (error) => {
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
|
||||
if ((error as any).code === 'ENOENT') {
|
||||
reject(createInstallationError({}));
|
||||
} else {
|
||||
reject(
|
||||
createAPICallError({
|
||||
message: `Failed to execute Grok CLI: ${error.message}`,
|
||||
code: (error as any).code,
|
||||
stderr: error.message,
|
||||
isRetryable: false
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
child.on('exit', (exitCode) => {
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
|
||||
resolve({
|
||||
stdout: stdout.trim(),
|
||||
stderr: stderr.trim(),
|
||||
exitCode: exitCode || 0
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate comprehensive warnings for unsupported parameters and validation issues
|
||||
*/
|
||||
private generateAllWarnings(
|
||||
options: LanguageModelV2CallOptions,
|
||||
prompt: string
|
||||
): LanguageModelV2CallWarning[] {
|
||||
const warnings: LanguageModelV2CallWarning[] = [];
|
||||
const unsupportedParams: string[] = [];
|
||||
|
||||
// Check for unsupported parameters
|
||||
if (options.temperature !== undefined)
|
||||
unsupportedParams.push('temperature');
|
||||
if (options.topP !== undefined) unsupportedParams.push('topP');
|
||||
if (options.topK !== undefined) unsupportedParams.push('topK');
|
||||
if (options.presencePenalty !== undefined)
|
||||
unsupportedParams.push('presencePenalty');
|
||||
if (options.frequencyPenalty !== undefined)
|
||||
unsupportedParams.push('frequencyPenalty');
|
||||
if (options.stopSequences !== undefined && options.stopSequences.length > 0)
|
||||
unsupportedParams.push('stopSequences');
|
||||
if (options.seed !== undefined) unsupportedParams.push('seed');
|
||||
|
||||
if (unsupportedParams.length > 0) {
|
||||
// Add a warning for each unsupported parameter
|
||||
for (const param of unsupportedParams) {
|
||||
warnings.push({
|
||||
type: 'unsupported-setting',
|
||||
setting: param as
|
||||
| 'temperature'
|
||||
| 'topP'
|
||||
| 'topK'
|
||||
| 'presencePenalty'
|
||||
| 'frequencyPenalty'
|
||||
| 'stopSequences'
|
||||
| 'seed',
|
||||
details: `Grok CLI does not support the ${param} parameter. It will be ignored.`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Add model validation warnings if needed
|
||||
if (!this.modelId || this.modelId.trim() === '') {
|
||||
warnings.push({
|
||||
type: 'other',
|
||||
message: 'Model ID is empty or invalid'
|
||||
});
|
||||
}
|
||||
|
||||
// Add prompt validation
|
||||
if (!prompt || prompt.trim() === '') {
|
||||
warnings.push({
|
||||
type: 'other',
|
||||
message: 'Prompt is empty'
|
||||
});
|
||||
}
|
||||
|
||||
return warnings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate text using Grok CLI
|
||||
*/
|
||||
async doGenerate(options: LanguageModelV2CallOptions) {
|
||||
// Handle abort signal early
|
||||
if (options.abortSignal?.aborted) {
|
||||
throw options.abortSignal.reason || new Error('Request aborted');
|
||||
}
|
||||
|
||||
// Check CLI installation
|
||||
const isInstalled = await this.checkGrokCliInstallation();
|
||||
if (!isInstalled) {
|
||||
throw createInstallationError({});
|
||||
}
|
||||
|
||||
// Get API key
|
||||
const apiKey = await this.getApiKey();
|
||||
if (!apiKey) {
|
||||
throw createAuthenticationError({
|
||||
message:
|
||||
'Grok CLI API key not found. Set GROK_CLI_API_KEY environment variable or configure grok-cli.'
|
||||
});
|
||||
}
|
||||
|
||||
const prompt = createPromptFromMessages(options.prompt);
|
||||
const warnings = this.generateAllWarnings(options, prompt);
|
||||
|
||||
// Build command arguments
|
||||
const args = ['--prompt', escapeShellArg(prompt)];
|
||||
|
||||
// Add model if specified
|
||||
if (this.modelId && this.modelId !== 'default') {
|
||||
args.push('--model', this.modelId);
|
||||
}
|
||||
|
||||
// Skip API key parameter if it's likely already configured to avoid hanging
|
||||
// The CLI seems to hang when trying to save API keys for grok-4 models
|
||||
// if (apiKey) {
|
||||
// args.push('--api-key', apiKey);
|
||||
// }
|
||||
|
||||
// Add base URL if provided in settings
|
||||
if (this.settings.baseURL) {
|
||||
args.push('--base-url', this.settings.baseURL);
|
||||
}
|
||||
|
||||
// Add working directory if specified
|
||||
if (this.settings.workingDirectory) {
|
||||
args.push('--directory', this.settings.workingDirectory);
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.executeGrokCli(args, { apiKey });
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
// Handle authentication errors
|
||||
if (
|
||||
result.stderr.toLowerCase().includes('unauthorized') ||
|
||||
result.stderr.toLowerCase().includes('authentication')
|
||||
) {
|
||||
throw createAuthenticationError({
|
||||
message: `Grok CLI authentication failed: ${result.stderr}`
|
||||
});
|
||||
}
|
||||
|
||||
throw createAPICallError({
|
||||
message: `Grok CLI failed with exit code ${result.exitCode}: ${result.stderr || 'Unknown error'}`,
|
||||
exitCode: result.exitCode,
|
||||
stderr: result.stderr,
|
||||
stdout: result.stdout,
|
||||
promptExcerpt: prompt.substring(0, 200),
|
||||
isRetryable: false
|
||||
});
|
||||
}
|
||||
|
||||
// Parse response
|
||||
const response = convertFromGrokCliResponse(result.stdout);
|
||||
let text = response.text || '';
|
||||
|
||||
// Extract JSON if in object-json mode
|
||||
const isObjectJson = (
|
||||
o: unknown
|
||||
): o is { mode: { type: 'object-json' } } =>
|
||||
!!o &&
|
||||
typeof o === 'object' &&
|
||||
'mode' in o &&
|
||||
(o as any).mode?.type === 'object-json';
|
||||
if (isObjectJson(options) && text) {
|
||||
text = extractJson(text);
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: text || ''
|
||||
}
|
||||
],
|
||||
usage: response.usage
|
||||
? {
|
||||
inputTokens: response.usage.promptTokens,
|
||||
outputTokens: response.usage.completionTokens,
|
||||
totalTokens: response.usage.totalTokens
|
||||
}
|
||||
: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
|
||||
finishReason: 'stop' as const,
|
||||
rawCall: {
|
||||
rawPrompt: prompt,
|
||||
rawSettings: args
|
||||
},
|
||||
warnings: warnings,
|
||||
response: {
|
||||
id: generateId(),
|
||||
timestamp: new Date(),
|
||||
modelId: this.modelId
|
||||
},
|
||||
request: {
|
||||
body: prompt
|
||||
},
|
||||
providerMetadata: {
|
||||
'grok-cli': {
|
||||
exitCode: result.exitCode,
|
||||
...(result.stderr && { stderr: result.stderr })
|
||||
}
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
// Re-throw our custom errors
|
||||
if (
|
||||
(error as any).name === 'APICallError' ||
|
||||
(error as any).name === 'LoadAPIKeyError'
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Wrap other errors
|
||||
throw createAPICallError({
|
||||
message: `Grok CLI execution failed: ${(error as Error).message}`,
|
||||
code: (error as any).code,
|
||||
promptExcerpt: prompt.substring(0, 200),
|
||||
isRetryable: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream text using Grok CLI
|
||||
* Note: Grok CLI doesn't natively support streaming, so this simulates streaming
|
||||
* by generating the full response and then streaming it in chunks
|
||||
*/
|
||||
async doStream(options: LanguageModelV2CallOptions) {
|
||||
const prompt = createPromptFromMessages(options.prompt);
|
||||
const warnings = this.generateAllWarnings(options, prompt);
|
||||
|
||||
const stream = new ReadableStream({
|
||||
start: async (controller) => {
|
||||
let abortListener: (() => void) | undefined;
|
||||
|
||||
try {
|
||||
// Handle abort signal
|
||||
if (options.abortSignal?.aborted) {
|
||||
throw options.abortSignal.reason || new Error('Request aborted');
|
||||
}
|
||||
|
||||
// Set up abort listener
|
||||
if (options.abortSignal) {
|
||||
abortListener = () => {
|
||||
controller.enqueue({
|
||||
type: 'error',
|
||||
error:
|
||||
options.abortSignal?.reason || new Error('Request aborted')
|
||||
});
|
||||
controller.close();
|
||||
};
|
||||
options.abortSignal.addEventListener('abort', abortListener, {
|
||||
once: true
|
||||
});
|
||||
}
|
||||
|
||||
// Emit stream-start with warnings
|
||||
controller.enqueue({ type: 'stream-start', warnings });
|
||||
|
||||
// Generate the full response first
|
||||
const result = await this.doGenerate(options);
|
||||
|
||||
// Emit response metadata
|
||||
controller.enqueue({
|
||||
type: 'response-metadata',
|
||||
id: result.response.id,
|
||||
timestamp: result.response.timestamp,
|
||||
modelId: result.response.modelId
|
||||
});
|
||||
|
||||
// Simulate streaming by chunking the text
|
||||
const content = result.content || [];
|
||||
const text =
|
||||
content.length > 0 && content[0].type === 'text'
|
||||
? content[0].text
|
||||
: '';
|
||||
const chunkSize = 50; // Characters per chunk
|
||||
let textPartId: string | undefined;
|
||||
|
||||
// Emit text-start if we have content
|
||||
if (text.length > 0) {
|
||||
textPartId = generateId();
|
||||
controller.enqueue({
|
||||
type: 'text-start',
|
||||
id: textPartId
|
||||
});
|
||||
}
|
||||
|
||||
for (let i = 0; i < text.length; i += chunkSize) {
|
||||
// Check for abort during streaming
|
||||
if (options.abortSignal?.aborted) {
|
||||
throw options.abortSignal.reason || new Error('Request aborted');
|
||||
}
|
||||
|
||||
const chunk = text.slice(i, i + chunkSize);
|
||||
controller.enqueue({
|
||||
type: 'text-delta',
|
||||
id: textPartId!,
|
||||
delta: chunk
|
||||
});
|
||||
|
||||
// Add small delay to simulate streaming
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
}
|
||||
|
||||
// Close text part if opened
|
||||
if (textPartId) {
|
||||
controller.enqueue({
|
||||
type: 'text-end',
|
||||
id: textPartId
|
||||
});
|
||||
}
|
||||
|
||||
// Emit finish event
|
||||
controller.enqueue({
|
||||
type: 'finish',
|
||||
finishReason: result.finishReason,
|
||||
usage: result.usage,
|
||||
providerMetadata: result.providerMetadata
|
||||
});
|
||||
|
||||
controller.close();
|
||||
} catch (error) {
|
||||
controller.enqueue({
|
||||
type: 'error',
|
||||
error
|
||||
});
|
||||
controller.close();
|
||||
} finally {
|
||||
// Clean up abort listener
|
||||
if (options.abortSignal && abortListener) {
|
||||
options.abortSignal.removeEventListener('abort', abortListener);
|
||||
}
|
||||
}
|
||||
},
|
||||
cancel: () => {
|
||||
// Clean up if stream is cancelled
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
stream,
|
||||
request: {
|
||||
body: prompt
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
/**
|
||||
* Tests for Grok CLI provider
|
||||
*/
|
||||
|
||||
import { NoSuchModelError } from '@ai-sdk/provider';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { GrokCliLanguageModel } from './grok-cli-language-model.js';
|
||||
import { createGrokCli, grokCli } from './grok-cli-provider.js';
|
||||
|
||||
// Mock the GrokCliLanguageModel
|
||||
vi.mock('./grok-cli-language-model.js', () => ({
|
||||
GrokCliLanguageModel: vi.fn().mockImplementation((options) => ({
|
||||
modelId: options.id,
|
||||
settings: options.settings,
|
||||
provider: 'grok-cli'
|
||||
}))
|
||||
}));
|
||||
|
||||
describe('createGrokCli', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should create a provider with default settings', () => {
|
||||
const provider = createGrokCli();
|
||||
expect(typeof provider).toBe('function');
|
||||
expect(typeof provider.languageModel).toBe('function');
|
||||
expect(typeof provider.chat).toBe('function');
|
||||
expect(typeof provider.textEmbeddingModel).toBe('function');
|
||||
expect(typeof provider.imageModel).toBe('function');
|
||||
});
|
||||
|
||||
it('should create a provider with custom default settings', () => {
|
||||
const defaultSettings = {
|
||||
timeout: 5000,
|
||||
workingDirectory: '/custom/path'
|
||||
};
|
||||
const provider = createGrokCli({ defaultSettings });
|
||||
|
||||
const model = provider('grok-2-mini');
|
||||
|
||||
expect(GrokCliLanguageModel).toHaveBeenCalledWith({
|
||||
id: 'grok-2-mini',
|
||||
settings: defaultSettings
|
||||
});
|
||||
});
|
||||
|
||||
it('should create language models with merged settings', () => {
|
||||
const defaultSettings = { timeout: 5000 };
|
||||
const provider = createGrokCli({ defaultSettings });
|
||||
|
||||
const modelSettings = { apiKey: 'test-key' };
|
||||
const model = provider('grok-2', modelSettings);
|
||||
|
||||
expect(GrokCliLanguageModel).toHaveBeenCalledWith({
|
||||
id: 'grok-2',
|
||||
settings: { timeout: 5000, apiKey: 'test-key' }
|
||||
});
|
||||
});
|
||||
|
||||
it('should create models via languageModel method', () => {
|
||||
const provider = createGrokCli();
|
||||
const model = provider.languageModel('grok-2-mini', { timeout: 1000 });
|
||||
|
||||
expect(GrokCliLanguageModel).toHaveBeenCalledWith({
|
||||
id: 'grok-2-mini',
|
||||
settings: { timeout: 1000 }
|
||||
});
|
||||
});
|
||||
|
||||
it('should create models via chat method (alias)', () => {
|
||||
const provider = createGrokCli();
|
||||
const model = provider.chat('grok-2');
|
||||
|
||||
expect(GrokCliLanguageModel).toHaveBeenCalledWith({
|
||||
id: 'grok-2',
|
||||
settings: {}
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw error when called with new keyword', () => {
|
||||
const provider = createGrokCli();
|
||||
expect(() => {
|
||||
// @ts-expect-error - intentionally testing invalid usage
|
||||
new provider('grok-2');
|
||||
}).toThrow(
|
||||
'The Grok CLI model function cannot be called with the new keyword.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw NoSuchModelError for textEmbeddingModel', () => {
|
||||
const provider = createGrokCli();
|
||||
expect(() => {
|
||||
provider.textEmbeddingModel('test-model');
|
||||
}).toThrow(NoSuchModelError);
|
||||
});
|
||||
|
||||
it('should throw NoSuchModelError for imageModel', () => {
|
||||
const provider = createGrokCli();
|
||||
expect(() => {
|
||||
provider.imageModel('test-model');
|
||||
}).toThrow(NoSuchModelError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('default grokCli provider', () => {
|
||||
it('should be a pre-configured provider instance', () => {
|
||||
expect(typeof grokCli).toBe('function');
|
||||
expect(typeof grokCli.languageModel).toBe('function');
|
||||
expect(typeof grokCli.chat).toBe('function');
|
||||
});
|
||||
|
||||
it('should create models with default configuration', () => {
|
||||
const model = grokCli('grok-2-mini');
|
||||
|
||||
expect(GrokCliLanguageModel).toHaveBeenCalledWith({
|
||||
id: 'grok-2-mini',
|
||||
settings: {}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,108 +0,0 @@
|
||||
/**
|
||||
* Grok CLI provider implementation for AI SDK v5
|
||||
*/
|
||||
|
||||
import type { LanguageModelV2, ProviderV2 } from '@ai-sdk/provider';
|
||||
import { NoSuchModelError } from '@ai-sdk/provider';
|
||||
import { GrokCliLanguageModel } from './grok-cli-language-model.js';
|
||||
import type { GrokCliModelId, GrokCliSettings } from './types.js';
|
||||
|
||||
/**
|
||||
* Grok CLI provider interface that extends the AI SDK's ProviderV2
|
||||
*/
|
||||
export interface GrokCliProvider extends ProviderV2 {
|
||||
/**
|
||||
* Creates a language model instance for the specified model ID.
|
||||
* This is a shorthand for calling `languageModel()`.
|
||||
*/
|
||||
(modelId: GrokCliModelId, settings?: GrokCliSettings): LanguageModelV2;
|
||||
|
||||
/**
|
||||
* Creates a language model instance for text generation.
|
||||
*/
|
||||
languageModel(
|
||||
modelId: GrokCliModelId,
|
||||
settings?: GrokCliSettings
|
||||
): LanguageModelV2;
|
||||
|
||||
/**
|
||||
* Alias for `languageModel()` to maintain compatibility with AI SDK patterns.
|
||||
*/
|
||||
chat(modelId: GrokCliModelId, settings?: GrokCliSettings): LanguageModelV2;
|
||||
|
||||
textEmbeddingModel(modelId: string): never;
|
||||
imageModel(modelId: string): never;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for creating a Grok CLI provider instance
|
||||
*/
|
||||
export interface GrokCliProviderSettings {
|
||||
/**
|
||||
* Default settings to use for all models created by this provider.
|
||||
* Individual model settings will override these defaults.
|
||||
*/
|
||||
defaultSettings?: GrokCliSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Grok CLI provider instance with the specified configuration.
|
||||
* The provider can be used to create language models for interacting with Grok models.
|
||||
*/
|
||||
export function createGrokCli(
|
||||
options: GrokCliProviderSettings = {}
|
||||
): GrokCliProvider {
|
||||
const createModel = (
|
||||
modelId: GrokCliModelId,
|
||||
settings: GrokCliSettings = {}
|
||||
): LanguageModelV2 => {
|
||||
const mergedSettings = {
|
||||
...options.defaultSettings,
|
||||
...settings
|
||||
};
|
||||
|
||||
return new GrokCliLanguageModel({
|
||||
id: modelId,
|
||||
settings: mergedSettings
|
||||
});
|
||||
};
|
||||
|
||||
const provider = function (
|
||||
modelId: GrokCliModelId,
|
||||
settings?: GrokCliSettings
|
||||
) {
|
||||
if (new.target) {
|
||||
throw new Error(
|
||||
'The Grok CLI model function cannot be called with the new keyword.'
|
||||
);
|
||||
}
|
||||
|
||||
return createModel(modelId, settings);
|
||||
};
|
||||
|
||||
provider.languageModel = createModel;
|
||||
provider.chat = createModel; // Alias for languageModel
|
||||
|
||||
// Add textEmbeddingModel method that throws NoSuchModelError
|
||||
provider.textEmbeddingModel = (modelId: string) => {
|
||||
throw new NoSuchModelError({
|
||||
modelId,
|
||||
modelType: 'textEmbeddingModel'
|
||||
});
|
||||
};
|
||||
|
||||
provider.imageModel = (modelId: string) => {
|
||||
throw new NoSuchModelError({
|
||||
modelId,
|
||||
modelType: 'imageModel'
|
||||
});
|
||||
};
|
||||
|
||||
return provider as GrokCliProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default Grok CLI provider instance.
|
||||
* Pre-configured provider for quick usage without custom settings.
|
||||
*/
|
||||
export const grokCli = createGrokCli();
|
||||
@@ -1,64 +0,0 @@
|
||||
/**
|
||||
* Provider exports for creating and configuring Grok CLI instances.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates a new Grok CLI provider instance and the default provider instance.
|
||||
*/
|
||||
export { createGrokCli, grokCli } from './grok-cli-provider.js';
|
||||
|
||||
/**
|
||||
* Type definitions for the Grok CLI provider.
|
||||
*/
|
||||
export type {
|
||||
GrokCliProvider,
|
||||
GrokCliProviderSettings
|
||||
} from './grok-cli-provider.js';
|
||||
|
||||
/**
|
||||
* Language model implementation for Grok CLI.
|
||||
* This class implements the AI SDK's LanguageModelV2 interface.
|
||||
*/
|
||||
export { GrokCliLanguageModel } from './grok-cli-language-model.js';
|
||||
|
||||
/**
|
||||
* Type definitions for Grok CLI language models.
|
||||
*/
|
||||
export type {
|
||||
GrokCliModelId,
|
||||
GrokCliLanguageModelOptions,
|
||||
GrokCliSettings,
|
||||
GrokCliMessage,
|
||||
GrokCliResponse,
|
||||
GrokCliErrorMetadata
|
||||
} from './types.js';
|
||||
|
||||
/**
|
||||
* Error handling utilities for Grok CLI.
|
||||
* These functions help create and identify specific error types.
|
||||
*/
|
||||
export {
|
||||
isAuthenticationError,
|
||||
isTimeoutError,
|
||||
isInstallationError,
|
||||
getErrorMetadata,
|
||||
createAPICallError,
|
||||
createAuthenticationError,
|
||||
createTimeoutError,
|
||||
createInstallationError
|
||||
} from './errors.js';
|
||||
|
||||
/**
|
||||
* Message conversion utilities for Grok CLI communication.
|
||||
*/
|
||||
export {
|
||||
convertToGrokCliMessages,
|
||||
convertFromGrokCliResponse,
|
||||
createPromptFromMessages,
|
||||
escapeShellArg
|
||||
} from './message-converter.js';
|
||||
|
||||
/**
|
||||
* JSON extraction utilities for parsing Grok responses.
|
||||
*/
|
||||
export { extractJson } from './json-extractor.js';
|
||||
@@ -1,81 +0,0 @@
|
||||
/**
|
||||
* Tests for JSON extraction utilities
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { extractJson } from './json-extractor.js';
|
||||
|
||||
describe('extractJson', () => {
|
||||
it('should extract JSON from markdown code blocks', () => {
|
||||
const text = '```json\n{"name": "test", "value": 42}\n```';
|
||||
const result = extractJson(text);
|
||||
expect(JSON.parse(result)).toEqual({ name: 'test', value: 42 });
|
||||
});
|
||||
|
||||
it('should extract JSON from generic code blocks', () => {
|
||||
const text = '```\n{"name": "test", "value": 42}\n```';
|
||||
const result = extractJson(text);
|
||||
expect(JSON.parse(result)).toEqual({ name: 'test', value: 42 });
|
||||
});
|
||||
|
||||
it('should remove JavaScript variable declarations', () => {
|
||||
const text = 'const result = {"name": "test", "value": 42};';
|
||||
const result = extractJson(text);
|
||||
expect(JSON.parse(result)).toEqual({ name: 'test', value: 42 });
|
||||
});
|
||||
|
||||
it('should handle let variable declarations', () => {
|
||||
const text = 'let data = {"name": "test", "value": 42};';
|
||||
const result = extractJson(text);
|
||||
expect(JSON.parse(result)).toEqual({ name: 'test', value: 42 });
|
||||
});
|
||||
|
||||
it('should handle var variable declarations', () => {
|
||||
const text = 'var config = {"name": "test", "value": 42};';
|
||||
const result = extractJson(text);
|
||||
expect(JSON.parse(result)).toEqual({ name: 'test', value: 42 });
|
||||
});
|
||||
|
||||
it('should extract JSON arrays', () => {
|
||||
const text = '[{"name": "test1"}, {"name": "test2"}]';
|
||||
const result = extractJson(text);
|
||||
expect(JSON.parse(result)).toEqual([{ name: 'test1' }, { name: 'test2' }]);
|
||||
});
|
||||
|
||||
it('should convert JavaScript object literals to JSON', () => {
|
||||
const text = "{name: 'test', value: 42}";
|
||||
const result = extractJson(text);
|
||||
expect(JSON.parse(result)).toEqual({ name: 'test', value: 42 });
|
||||
});
|
||||
|
||||
it('should return valid JSON (canonical formatting)', () => {
|
||||
const text = '{"name": "test", "value": 42}';
|
||||
const result = extractJson(text);
|
||||
expect(JSON.parse(result)).toEqual({ name: 'test', value: 42 });
|
||||
});
|
||||
|
||||
it('should return original text when JSON parsing fails completely', () => {
|
||||
const text = 'This is not JSON at all';
|
||||
const result = extractJson(text);
|
||||
expect(result).toBe('This is not JSON at all');
|
||||
});
|
||||
|
||||
it('should handle complex nested objects', () => {
|
||||
const text =
|
||||
'```json\n{\n "user": {\n "name": "John",\n "age": 30\n },\n "items": [1, 2, 3]\n}\n```';
|
||||
const result = extractJson(text);
|
||||
expect(JSON.parse(result)).toEqual({
|
||||
user: {
|
||||
name: 'John',
|
||||
age: 30
|
||||
},
|
||||
items: [1, 2, 3]
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle mixed quotes in object literals', () => {
|
||||
const text = `{name: "test", value: 'mixed quotes'}`;
|
||||
const result = extractJson(text);
|
||||
expect(JSON.parse(result)).toEqual({ name: 'test', value: 'mixed quotes' });
|
||||
});
|
||||
});
|
||||
@@ -1,132 +0,0 @@
|
||||
/**
|
||||
* Extract JSON from AI's response using a tolerant parser.
|
||||
*
|
||||
* The function removes common wrappers such as markdown fences or variable
|
||||
* declarations and then attempts to parse the remaining text with
|
||||
* `jsonc-parser`. If valid JSON (or JSONC) can be parsed, it is returned as a
|
||||
* string via `JSON.stringify`. Otherwise the original text is returned.
|
||||
*
|
||||
* @param text - Raw text which may contain JSON
|
||||
* @returns A valid JSON string if extraction succeeds, otherwise the original text
|
||||
*/
|
||||
import { parse, type ParseError } from 'jsonc-parser';
|
||||
|
||||
export function extractJson(text: string): string {
|
||||
let content = text.trim();
|
||||
|
||||
// Strip ```json or ``` fences
|
||||
const fenceMatch = /```(?:json)?\s*([\s\S]*?)\s*```/i.exec(content);
|
||||
if (fenceMatch) {
|
||||
content = fenceMatch[1];
|
||||
}
|
||||
|
||||
// Strip variable declarations like `const foo =` or `let foo =`
|
||||
const varMatch = /^\s*(?:const|let|var)\s+\w+\s*=\s*([\s\S]*)/i.exec(content);
|
||||
if (varMatch) {
|
||||
content = varMatch[1];
|
||||
// Remove trailing semicolon if present
|
||||
if (content.trim().endsWith(';')) {
|
||||
content = content.trim().slice(0, -1);
|
||||
}
|
||||
}
|
||||
|
||||
// Find the first opening bracket
|
||||
const firstObj = content.indexOf('{');
|
||||
const firstArr = content.indexOf('[');
|
||||
if (firstObj === -1 && firstArr === -1) {
|
||||
return text;
|
||||
}
|
||||
const start =
|
||||
firstArr === -1
|
||||
? firstObj
|
||||
: firstObj === -1
|
||||
? firstArr
|
||||
: Math.min(firstObj, firstArr);
|
||||
content = content.slice(start);
|
||||
|
||||
// Try to parse the entire string with jsonc-parser
|
||||
const tryParse = (value: string): string | undefined => {
|
||||
const errors: ParseError[] = [];
|
||||
try {
|
||||
const result = parse(value, errors, { allowTrailingComma: true });
|
||||
if (errors.length === 0) {
|
||||
return JSON.stringify(result, null, 2);
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const parsed = tryParse(content);
|
||||
if (parsed !== undefined) {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
// If parsing the full string failed, use a more efficient approach
|
||||
// to find valid JSON boundaries
|
||||
const openChar = content[0];
|
||||
const closeChar = openChar === '{' ? '}' : ']';
|
||||
|
||||
// Find all potential closing positions by tracking nesting depth
|
||||
const closingPositions: number[] = [];
|
||||
let depth = 0;
|
||||
let inString = false;
|
||||
let escapeNext = false;
|
||||
|
||||
for (let i = 0; i < content.length; i++) {
|
||||
const char = content[i];
|
||||
|
||||
if (escapeNext) {
|
||||
escapeNext = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === '\\') {
|
||||
escapeNext = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === '"' && !inString) {
|
||||
inString = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === '"' && inString) {
|
||||
inString = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip content inside strings
|
||||
if (inString) continue;
|
||||
|
||||
if (char === openChar) {
|
||||
depth++;
|
||||
} else if (char === closeChar) {
|
||||
depth--;
|
||||
if (depth === 0) {
|
||||
closingPositions.push(i + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try parsing at each valid closing position, starting from the end
|
||||
for (let i = closingPositions.length - 1; i >= 0; i--) {
|
||||
const attempt = tryParse(content.slice(0, closingPositions[i]));
|
||||
if (attempt !== undefined) {
|
||||
return attempt;
|
||||
}
|
||||
}
|
||||
|
||||
// As a final fallback, try the original character-by-character approach
|
||||
// but only for the last 1000 characters to limit performance impact
|
||||
const searchStart = Math.max(0, content.length - 1000);
|
||||
for (let end = content.length - 1; end > searchStart; end--) {
|
||||
const attempt = tryParse(content.slice(0, end));
|
||||
if (attempt !== undefined) {
|
||||
return attempt;
|
||||
}
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
/**
|
||||
* Tests for message conversion utilities
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
convertFromGrokCliResponse,
|
||||
convertToGrokCliMessages,
|
||||
createPromptFromMessages,
|
||||
escapeShellArg
|
||||
} from './message-converter.js';
|
||||
|
||||
describe('convertToGrokCliMessages', () => {
|
||||
it('should convert string content messages', () => {
|
||||
const messages = [
|
||||
{ role: 'user', content: 'Hello, world!' },
|
||||
{ role: 'assistant', content: 'Hi there!' }
|
||||
];
|
||||
|
||||
const result = convertToGrokCliMessages(messages);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ role: 'user', content: 'Hello, world!' },
|
||||
{ role: 'assistant', content: 'Hi there!' }
|
||||
]);
|
||||
});
|
||||
|
||||
it('should convert array content messages', () => {
|
||||
const messages = [
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
{ type: 'text', text: 'Hello' },
|
||||
{ type: 'text', text: 'World' }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const result = convertToGrokCliMessages(messages);
|
||||
|
||||
expect(result).toEqual([{ role: 'user', content: 'Hello\nWorld' }]);
|
||||
});
|
||||
|
||||
it('should convert object content messages', () => {
|
||||
const messages = [
|
||||
{
|
||||
role: 'user',
|
||||
content: { text: 'Hello from object' }
|
||||
}
|
||||
];
|
||||
|
||||
const result = convertToGrokCliMessages(messages);
|
||||
|
||||
expect(result).toEqual([{ role: 'user', content: 'Hello from object' }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertFromGrokCliResponse', () => {
|
||||
it('should parse JSONL response format', () => {
|
||||
const responseText = `{"role": "assistant", "content": "Hello there!", "usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15}}`;
|
||||
|
||||
const result = convertFromGrokCliResponse(responseText);
|
||||
|
||||
expect(result).toEqual({
|
||||
text: 'Hello there!',
|
||||
usage: {
|
||||
promptTokens: 10,
|
||||
completionTokens: 5,
|
||||
totalTokens: 15
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle multiple lines in JSONL format', () => {
|
||||
const responseText = `{"role": "user", "content": "Hello"}
|
||||
{"role": "assistant", "content": "Hi there!", "usage": {"prompt_tokens": 5, "completion_tokens": 3}}`;
|
||||
|
||||
const result = convertFromGrokCliResponse(responseText);
|
||||
|
||||
expect(result).toEqual({
|
||||
text: 'Hi there!',
|
||||
usage: {
|
||||
promptTokens: 5,
|
||||
completionTokens: 3,
|
||||
totalTokens: 0
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should fallback to raw text when parsing fails', () => {
|
||||
const responseText = 'Invalid JSON response';
|
||||
|
||||
const result = convertFromGrokCliResponse(responseText);
|
||||
|
||||
expect(result).toEqual({
|
||||
text: 'Invalid JSON response',
|
||||
usage: undefined
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createPromptFromMessages', () => {
|
||||
it('should create formatted prompt from messages', () => {
|
||||
const messages = [
|
||||
{ role: 'system', content: 'You are a helpful assistant.' },
|
||||
{ role: 'user', content: 'What is 2+2?' },
|
||||
{ role: 'assistant', content: '2+2 equals 4.' }
|
||||
];
|
||||
|
||||
const result = createPromptFromMessages(messages);
|
||||
|
||||
expect(result).toBe(
|
||||
'System: You are a helpful assistant.\n\nUser: What is 2+2?\n\nAssistant: 2+2 equals 4.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle custom role names', () => {
|
||||
const messages = [{ role: 'custom', content: 'Custom message' }];
|
||||
|
||||
const result = createPromptFromMessages(messages);
|
||||
|
||||
expect(result).toBe('custom: Custom message');
|
||||
});
|
||||
|
||||
it('should trim whitespace from message content', () => {
|
||||
const messages = [
|
||||
{ role: 'user', content: ' Hello with spaces ' },
|
||||
{ role: 'assistant', content: '\n\nResponse with newlines\n\n' }
|
||||
];
|
||||
|
||||
const result = createPromptFromMessages(messages);
|
||||
|
||||
expect(result).toBe(
|
||||
'User: Hello with spaces\n\nAssistant: Response with newlines'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('escapeShellArg', () => {
|
||||
it('should escape single quotes', () => {
|
||||
const arg = "It's a test";
|
||||
const result = escapeShellArg(arg);
|
||||
expect(result).toBe("'It'\\''s a test'");
|
||||
});
|
||||
|
||||
it('should handle strings without special characters', () => {
|
||||
const arg = 'simple string';
|
||||
const result = escapeShellArg(arg);
|
||||
expect(result).toBe("'simple string'");
|
||||
});
|
||||
|
||||
it('should convert non-string values to strings', () => {
|
||||
const arg = 123;
|
||||
const result = escapeShellArg(arg);
|
||||
expect(result).toBe("'123'");
|
||||
});
|
||||
|
||||
it('should handle empty strings', () => {
|
||||
const arg = '';
|
||||
const result = escapeShellArg(arg);
|
||||
expect(result).toBe("''");
|
||||
});
|
||||
});
|
||||
@@ -1,153 +0,0 @@
|
||||
/**
|
||||
* Message format conversion utilities for Grok CLI provider
|
||||
*/
|
||||
|
||||
import type { GrokCliMessage, GrokCliResponse } from './types.js';
|
||||
|
||||
/**
|
||||
* AI SDK message type (simplified interface)
|
||||
*/
|
||||
interface AISDKMessage {
|
||||
role: string;
|
||||
content:
|
||||
| string
|
||||
| Array<{ type: string; text?: string }>
|
||||
| { text?: string; [key: string]: unknown };
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert AI SDK messages to Grok CLI compatible format
|
||||
* @param messages - AI SDK message array
|
||||
* @returns Grok CLI compatible messages
|
||||
*/
|
||||
export function convertToGrokCliMessages(
|
||||
messages: AISDKMessage[]
|
||||
): GrokCliMessage[] {
|
||||
return messages.map((message) => {
|
||||
// Handle different message content types
|
||||
let content = '';
|
||||
|
||||
if (typeof message.content === 'string') {
|
||||
content = message.content;
|
||||
} else if (Array.isArray(message.content)) {
|
||||
// Handle multi-part content (text and images)
|
||||
content = message.content
|
||||
.filter((part) => part.type === 'text')
|
||||
.map((part) => part.text || '')
|
||||
.join('\n');
|
||||
} else if (message.content && typeof message.content === 'object') {
|
||||
// Handle object content
|
||||
content = message.content.text || JSON.stringify(message.content);
|
||||
}
|
||||
|
||||
return {
|
||||
role: message.role,
|
||||
content: content.trim()
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Grok CLI response to AI SDK format
|
||||
* @param responseText - Raw response text from Grok CLI (JSONL format)
|
||||
* @returns AI SDK compatible response object
|
||||
*/
|
||||
export function convertFromGrokCliResponse(responseText: string): {
|
||||
text: string;
|
||||
usage?: {
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
totalTokens: number;
|
||||
};
|
||||
} {
|
||||
try {
|
||||
// Grok CLI outputs JSONL format - each line is a separate JSON message
|
||||
const lines = responseText
|
||||
.trim()
|
||||
.split('\n')
|
||||
.filter((line) => line.trim());
|
||||
|
||||
// Parse each line as JSON and find assistant messages
|
||||
const messages: GrokCliResponse[] = [];
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const message = JSON.parse(line) as GrokCliResponse;
|
||||
messages.push(message);
|
||||
} catch (parseError) {
|
||||
// Skip invalid JSON lines
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Find the last assistant message
|
||||
const assistantMessage = messages
|
||||
.filter((msg) => msg.role === 'assistant')
|
||||
.pop();
|
||||
|
||||
if (assistantMessage && assistantMessage.content) {
|
||||
return {
|
||||
text: assistantMessage.content,
|
||||
usage: assistantMessage.usage
|
||||
? {
|
||||
promptTokens: assistantMessage.usage.prompt_tokens || 0,
|
||||
completionTokens: assistantMessage.usage.completion_tokens || 0,
|
||||
totalTokens: assistantMessage.usage.total_tokens || 0
|
||||
}
|
||||
: undefined
|
||||
};
|
||||
}
|
||||
|
||||
// Fallback: if no assistant message found, return the raw text
|
||||
return {
|
||||
text: responseText.trim(),
|
||||
usage: undefined
|
||||
};
|
||||
} catch (error) {
|
||||
// If parsing fails completely, treat as plain text response
|
||||
return {
|
||||
text: responseText.trim(),
|
||||
usage: undefined
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a prompt string for Grok CLI from messages
|
||||
* @param messages - AI SDK message array
|
||||
* @returns Formatted prompt string
|
||||
*/
|
||||
export function createPromptFromMessages(messages: AISDKMessage[]): string {
|
||||
const grokMessages = convertToGrokCliMessages(messages);
|
||||
|
||||
// Create a conversation-style prompt
|
||||
const prompt = grokMessages
|
||||
.map((message) => {
|
||||
switch (message.role) {
|
||||
case 'system':
|
||||
return `System: ${message.content}`;
|
||||
case 'user':
|
||||
return `User: ${message.content}`;
|
||||
case 'assistant':
|
||||
return `Assistant: ${message.content}`;
|
||||
default:
|
||||
return `${message.role}: ${message.content}`;
|
||||
}
|
||||
})
|
||||
.join('\n\n');
|
||||
|
||||
return prompt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape shell arguments for safe CLI execution
|
||||
* @param arg - Argument to escape
|
||||
* @returns Shell-escaped argument
|
||||
*/
|
||||
export function escapeShellArg(arg: string | unknown): string {
|
||||
if (typeof arg !== 'string') {
|
||||
arg = String(arg);
|
||||
}
|
||||
|
||||
// Replace single quotes with '\''
|
||||
return "'" + (arg as string).replace(/'/g, "'\\''") + "'";
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
/**
|
||||
* Type definitions for Grok CLI provider
|
||||
*/
|
||||
|
||||
/**
|
||||
* Settings for configuring Grok CLI behavior
|
||||
*/
|
||||
export interface GrokCliSettings {
|
||||
/** API key for Grok CLI */
|
||||
apiKey?: string;
|
||||
/** Base URL for Grok API */
|
||||
baseURL?: string;
|
||||
/** Default model to use */
|
||||
model?: string;
|
||||
/** Timeout in milliseconds */
|
||||
timeout?: number;
|
||||
/** Working directory for CLI commands */
|
||||
workingDirectory?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Model identifiers supported by Grok CLI
|
||||
*/
|
||||
export type GrokCliModelId = string;
|
||||
|
||||
/**
|
||||
* Error metadata for Grok CLI operations
|
||||
*/
|
||||
export interface GrokCliErrorMetadata {
|
||||
/** Error code */
|
||||
code?: string;
|
||||
/** Process exit code */
|
||||
exitCode?: number;
|
||||
/** Standard error output */
|
||||
stderr?: string;
|
||||
/** Standard output */
|
||||
stdout?: string;
|
||||
/** Excerpt of the prompt that caused the error */
|
||||
promptExcerpt?: string;
|
||||
/** Timeout value in milliseconds */
|
||||
timeoutMs?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Message format for Grok CLI communication
|
||||
*/
|
||||
export interface GrokCliMessage {
|
||||
/** Message role (user, assistant, system) */
|
||||
role: string;
|
||||
/** Message content */
|
||||
content: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Response format from Grok CLI
|
||||
*/
|
||||
export interface GrokCliResponse {
|
||||
/** Message role */
|
||||
role: string;
|
||||
/** Response content */
|
||||
content: string;
|
||||
/** Token usage information */
|
||||
usage?: {
|
||||
/** Input tokens used */
|
||||
prompt_tokens?: number;
|
||||
/** Output tokens used */
|
||||
completion_tokens?: number;
|
||||
/** Total tokens used */
|
||||
total_tokens?: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for Grok CLI language model
|
||||
*/
|
||||
export interface GrokCliLanguageModelOptions {
|
||||
/** Model identifier */
|
||||
id: GrokCliModelId;
|
||||
/** Model settings */
|
||||
settings?: GrokCliSettings;
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2022"],
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"baseUrl": ".",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"strictBindCallApply": true,
|
||||
"strictPropertyInitialization": true,
|
||||
"noImplicitThis": true,
|
||||
"alwaysStrict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"moduleResolution": "bundler",
|
||||
"moduleDetection": "force",
|
||||
"types": ["node"],
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"allowImportingTsExtensions": false
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "tests", "**/*.test.ts", "**/*.spec.ts"]
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
# @tm/build-config
|
||||
|
||||
## null
|
||||
|
||||
## 1.0.1
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"name": "@tm/build-config",
|
||||
"version": "1.0.0",
|
||||
"description": "Shared build configuration for Task Master monorepo",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
@@ -20,7 +21,8 @@
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.9.2"
|
||||
"dotenv-mono": "^1.5.1",
|
||||
"typescript": "^5.7.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"tsup": "^8.5.0"
|
||||
|
||||
@@ -27,11 +27,6 @@ export const baseConfig: Partial<UserConfig> = {
|
||||
dts: isDevelopment,
|
||||
minify: isProduction,
|
||||
treeshake: isProduction,
|
||||
// Better debugging in development
|
||||
...(isDevelopment && {
|
||||
keepNames: true,
|
||||
splitting: false // Disable code splitting for better stack traces
|
||||
}),
|
||||
// Keep all npm dependencies external (available via node_modules)
|
||||
external: [/^[^@./]/, /^@(?!tm\/)/]
|
||||
};
|
||||
@@ -43,9 +38,9 @@ export const baseConfig: Partial<UserConfig> = {
|
||||
export function mergeConfig(
|
||||
base: Partial<UserConfig>,
|
||||
overrides: Partial<UserConfig>
|
||||
): UserConfig {
|
||||
): Partial<UserConfig> {
|
||||
return {
|
||||
...base,
|
||||
...overrides
|
||||
} as UserConfig;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## null
|
||||
|
||||
## 0.26.1
|
||||
|
||||
All notable changes to the @task-master/tm-core package will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
@@ -12,7 +8,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Initial package structure and configuration
|
||||
- TypeScript support with strict mode
|
||||
- Dual ESM/CJS build system with tsup
|
||||
@@ -23,7 +18,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Comprehensive documentation and README
|
||||
|
||||
### Development Infrastructure
|
||||
|
||||
- tsup configuration for dual format builds
|
||||
- Jest configuration with ESM support
|
||||
- ESLint configuration with TypeScript rules
|
||||
@@ -33,7 +27,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- .gitignore for development files
|
||||
|
||||
### Package Structure
|
||||
|
||||
- `src/types/` - TypeScript type definitions (placeholder)
|
||||
- `src/providers/` - AI provider implementations (placeholder)
|
||||
- `src/storage/` - Storage layer abstractions (placeholder)
|
||||
@@ -45,7 +38,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
## [1.0.0] - TBD
|
||||
|
||||
### Planned Features
|
||||
|
||||
- Complete TypeScript type system
|
||||
- AI provider implementations
|
||||
- Storage adapters
|
||||
@@ -60,11 +52,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
## Release Notes
|
||||
|
||||
### Version 1.0.0 (Coming Soon)
|
||||
|
||||
This will be the first stable release of tm-core with complete implementations of all modules. Currently, all modules contain placeholder implementations to establish the package structure and enable development of dependent packages.
|
||||
|
||||
### Development Status
|
||||
|
||||
- ✅ Package structure and configuration
|
||||
- ✅ Build and test infrastructure
|
||||
- ✅ Development tooling setup
|
||||
@@ -77,4 +67,4 @@ This will be the first stable release of tm-core with complete implementations o
|
||||
- 🚧 Configuration system (Task 122)
|
||||
- 🚧 Testing infrastructure (Task 123)
|
||||
- 🚧 Documentation (Task 124)
|
||||
- 🚧 Package finalization (Task 125)
|
||||
- 🚧 Package finalization (Task 125)
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"name": "@tm/core",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "Core library for Task Master - TypeScript task management system",
|
||||
"type": "module",
|
||||
@@ -31,13 +32,21 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@supabase/supabase-js": "^2.57.4",
|
||||
"zod": "^4.1.11"
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.5",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"typescript": "^5.9.2",
|
||||
"vitest": "^3.2.4"
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@tm/build-config": "*",
|
||||
"@types/node": "^20.11.30",
|
||||
"@vitest/coverage-v8": "^2.0.5",
|
||||
"dotenv-mono": "^1.3.14",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsup": "^8.5.0",
|
||||
"typescript": "^5.4.3",
|
||||
"vitest": "^2.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"files": ["src", "README.md", "CHANGELOG.md"],
|
||||
"keywords": ["task-management", "typescript", "ai", "prd", "parser"],
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
||||
import { ConfigManager } from './config-manager.js';
|
||||
import { DEFAULT_CONFIG_VALUES } from '../interfaces/configuration.interface.js';
|
||||
import { ConfigLoader } from './services/config-loader.service.js';
|
||||
import { ConfigMerger } from './services/config-merger.service.js';
|
||||
import { RuntimeStateManager } from './services/runtime-state-manager.service.js';
|
||||
@@ -70,8 +69,8 @@ describe('ConfigManager', () => {
|
||||
({
|
||||
loadState: vi.fn().mockResolvedValue({ activeTag: 'master' }),
|
||||
saveState: vi.fn().mockResolvedValue(undefined),
|
||||
getCurrentTag: vi.fn().mockReturnValue('master'),
|
||||
setCurrentTag: vi.fn().mockResolvedValue(undefined),
|
||||
getActiveTag: vi.fn().mockReturnValue('master'),
|
||||
setActiveTag: vi.fn().mockResolvedValue(undefined),
|
||||
getState: vi.fn().mockReturnValue({ activeTag: 'master' }),
|
||||
updateMetadata: vi.fn().mockResolvedValue(undefined),
|
||||
clearState: vi.fn().mockResolvedValue(undefined)
|
||||
@@ -228,8 +227,8 @@ describe('ConfigManager', () => {
|
||||
|
||||
const models = manager.getModelConfig();
|
||||
expect(models).toEqual({
|
||||
main: DEFAULT_CONFIG_VALUES.MODELS.MAIN,
|
||||
fallback: DEFAULT_CONFIG_VALUES.MODELS.FALLBACK
|
||||
main: 'claude-3-5-sonnet-20241022',
|
||||
fallback: 'gpt-4o-mini'
|
||||
});
|
||||
});
|
||||
|
||||
@@ -282,7 +281,7 @@ describe('ConfigManager', () => {
|
||||
await manager.setActiveTag('feature-branch');
|
||||
|
||||
const stateManager = (manager as any).stateManager;
|
||||
expect(stateManager.setCurrentTag).toHaveBeenCalledWith('feature-branch');
|
||||
expect(stateManager.setActiveTag).toHaveBeenCalledWith('feature-branch');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import type {
|
||||
PartialConfiguration,
|
||||
RuntimeStorageConfig
|
||||
} from '../interfaces/configuration.interface.js';
|
||||
import { DEFAULT_CONFIG_VALUES as DEFAULTS } from '../interfaces/configuration.interface.js';
|
||||
import { ConfigLoader } from './services/config-loader.service.js';
|
||||
import {
|
||||
ConfigMerger,
|
||||
@@ -168,8 +167,8 @@ export class ConfigManager {
|
||||
getModelConfig() {
|
||||
return (
|
||||
this.config.models || {
|
||||
main: DEFAULTS.MODELS.MAIN,
|
||||
fallback: DEFAULTS.MODELS.FALLBACK
|
||||
main: 'claude-3-5-sonnet-20241022',
|
||||
fallback: 'gpt-4o-mini'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
/**
|
||||
* Base executor class providing common functionality for all executors
|
||||
*/
|
||||
|
||||
import type { Task } from '../types/index.js';
|
||||
import type { ITaskExecutor, ExecutorType, ExecutionResult } from './types.js';
|
||||
import { getLogger } from '../logger/index.js';
|
||||
|
||||
export abstract class BaseExecutor implements ITaskExecutor {
|
||||
protected readonly logger = getLogger('BaseExecutor');
|
||||
protected readonly projectRoot: string;
|
||||
protected readonly config: Record<string, any>;
|
||||
|
||||
constructor(projectRoot: string, config: Record<string, any> = {}) {
|
||||
this.projectRoot = projectRoot;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
abstract execute(task: Task): Promise<ExecutionResult>;
|
||||
abstract getType(): ExecutorType;
|
||||
abstract isAvailable(): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Format task details into a readable prompt
|
||||
*/
|
||||
protected formatTaskPrompt(task: Task): string {
|
||||
const sections: string[] = [];
|
||||
|
||||
sections.push(`Task ID: ${task.id}`);
|
||||
sections.push(`Title: ${task.title}`);
|
||||
|
||||
if (task.description) {
|
||||
sections.push(`\nDescription:\n${task.description}`);
|
||||
}
|
||||
|
||||
if (task.details) {
|
||||
sections.push(`\nImplementation Details:\n${task.details}`);
|
||||
}
|
||||
|
||||
if (task.testStrategy) {
|
||||
sections.push(`\nTest Strategy:\n${task.testStrategy}`);
|
||||
}
|
||||
|
||||
if (task.dependencies && task.dependencies.length > 0) {
|
||||
sections.push(`\nDependencies: ${task.dependencies.join(', ')}`);
|
||||
}
|
||||
|
||||
if (task.subtasks && task.subtasks.length > 0) {
|
||||
const subtaskList = task.subtasks
|
||||
.map((st) => ` - [${st.status}] ${st.id}: ${st.title}`)
|
||||
.join('\n');
|
||||
sections.push(`\nSubtasks:\n${subtaskList}`);
|
||||
}
|
||||
|
||||
sections.push(`\nStatus: ${task.status}`);
|
||||
sections.push(`Priority: ${task.priority}`);
|
||||
|
||||
return sections.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create base execution result
|
||||
*/
|
||||
protected createResult(
|
||||
taskId: string,
|
||||
success: boolean,
|
||||
output?: string,
|
||||
error?: string
|
||||
): ExecutionResult {
|
||||
return {
|
||||
success,
|
||||
taskId,
|
||||
executorType: this.getType(),
|
||||
output,
|
||||
error,
|
||||
startTime: new Date().toISOString(),
|
||||
endTime: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user