Compare commits
4 Commits
ralph/fix/
...
ralph/feat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
943356221c | ||
|
|
9923b4f486 | ||
|
|
6bc75c0ac6 | ||
|
|
d7fca1844f |
5
.changeset/metal-rocks-help.md
Normal file
5
.changeset/metal-rocks-help.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"task-master-ai": minor
|
||||
---
|
||||
|
||||
Improve next command to work with remote
|
||||
5
.changeset/open-tips-notice.md
Normal file
5
.changeset/open-tips-notice.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"task-master-ai": minor
|
||||
---
|
||||
|
||||
Add 4.5 haiku and sonnet to supported models for claude-code and anthropic ai providers
|
||||
@@ -8,6 +8,7 @@ import { Command } from 'commander';
|
||||
// Import all commands
|
||||
import { ListTasksCommand } from './commands/list.command.js';
|
||||
import { ShowCommand } from './commands/show.command.js';
|
||||
import { NextCommand } from './commands/next.command.js';
|
||||
import { AuthCommand } from './commands/auth.command.js';
|
||||
import { ContextCommand } from './commands/context.command.js';
|
||||
import { StartCommand } from './commands/start.command.js';
|
||||
@@ -45,6 +46,12 @@ export class CommandRegistry {
|
||||
commandClass: ShowCommand as any,
|
||||
category: 'task'
|
||||
},
|
||||
{
|
||||
name: 'next',
|
||||
description: 'Find the next available task to work on',
|
||||
commandClass: NextCommand as any,
|
||||
category: 'task'
|
||||
},
|
||||
{
|
||||
name: 'start',
|
||||
description: 'Start working on a task with claude-code',
|
||||
|
||||
247
apps/cli/src/commands/next.command.ts
Normal file
247
apps/cli/src/commands/next.command.ts
Normal file
@@ -0,0 +1,247 @@
|
||||
/**
|
||||
* @fileoverview NextCommand using Commander's native class pattern
|
||||
* Extends Commander.Command for better integration with the framework
|
||||
*/
|
||||
|
||||
import path from 'node:path';
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import boxen from 'boxen';
|
||||
import { createTaskMasterCore, type Task, type TaskMasterCore } from '@tm/core';
|
||||
import type { StorageType } from '@tm/core/types';
|
||||
import { displayTaskDetails } from '../ui/components/task-detail.component.js';
|
||||
import { displayHeader } from '../ui/index.js';
|
||||
|
||||
/**
|
||||
* Options interface for the next command
|
||||
*/
|
||||
export interface NextCommandOptions {
|
||||
tag?: string;
|
||||
format?: 'text' | 'json';
|
||||
silent?: boolean;
|
||||
project?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result type from next command
|
||||
*/
|
||||
export interface NextTaskResult {
|
||||
task: Task | null;
|
||||
found: boolean;
|
||||
tag: string;
|
||||
storageType: Exclude<StorageType, 'auto'>;
|
||||
}
|
||||
|
||||
/**
|
||||
* NextCommand extending Commander's Command class
|
||||
* This is a thin presentation layer over @tm/core
|
||||
*/
|
||||
export class NextCommand extends Command {
|
||||
private tmCore?: TaskMasterCore;
|
||||
private lastResult?: NextTaskResult;
|
||||
|
||||
constructor(name?: string) {
|
||||
super(name || 'next');
|
||||
|
||||
// Configure the command
|
||||
this.description('Find the next available task to work on')
|
||||
.option('-t, --tag <tag>', 'Filter by tag')
|
||||
.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: NextCommandOptions) => {
|
||||
await this.executeCommand(options);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the next command
|
||||
*/
|
||||
private async executeCommand(options: NextCommandOptions): Promise<void> {
|
||||
try {
|
||||
// Validate options (throws on invalid options)
|
||||
this.validateOptions(options);
|
||||
|
||||
// Initialize tm-core
|
||||
await this.initializeCore(options.project || process.cwd());
|
||||
|
||||
// Get next task from core
|
||||
const result = await this.getNextTask(options);
|
||||
|
||||
// Store result for programmatic access
|
||||
this.setLastResult(result);
|
||||
|
||||
// Display results
|
||||
if (!options.silent) {
|
||||
this.displayResults(result, options);
|
||||
}
|
||||
} catch (error: any) {
|
||||
const msg = error?.getSanitizedDetails?.() ?? {
|
||||
message: error?.message ?? String(error)
|
||||
};
|
||||
|
||||
// Allow error to propagate for library compatibility
|
||||
throw new Error(msg.message || 'Unexpected error in next command');
|
||||
} finally {
|
||||
// Always clean up resources, even on error
|
||||
await this.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate command options
|
||||
*/
|
||||
private validateOptions(options: NextCommandOptions): void {
|
||||
// Validate format
|
||||
if (options.format && !['text', 'json'].includes(options.format)) {
|
||||
throw new Error(
|
||||
`Invalid format: ${options.format}. Valid formats are: text, json`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize TaskMasterCore
|
||||
*/
|
||||
private async initializeCore(projectRoot: string): Promise<void> {
|
||||
if (!this.tmCore) {
|
||||
const resolved = path.resolve(projectRoot);
|
||||
this.tmCore = await createTaskMasterCore({ projectPath: resolved });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get next task from tm-core
|
||||
*/
|
||||
private async getNextTask(
|
||||
options: NextCommandOptions
|
||||
): Promise<NextTaskResult> {
|
||||
if (!this.tmCore) {
|
||||
throw new Error('TaskMasterCore not initialized');
|
||||
}
|
||||
|
||||
// Call tm-core to get next task
|
||||
const task = await this.tmCore.getNextTask(options.tag);
|
||||
|
||||
// Get storage type and active tag
|
||||
const storageType = this.tmCore.getStorageType();
|
||||
if (storageType === 'auto') {
|
||||
throw new Error('Storage type must be resolved before use');
|
||||
}
|
||||
const activeTag = options.tag || this.tmCore.getActiveTag();
|
||||
|
||||
return {
|
||||
task,
|
||||
found: task !== null,
|
||||
tag: activeTag,
|
||||
storageType
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Display results based on format
|
||||
*/
|
||||
private displayResults(
|
||||
result: NextTaskResult,
|
||||
options: NextCommandOptions
|
||||
): void {
|
||||
const format = options.format || 'text';
|
||||
|
||||
switch (format) {
|
||||
case 'json':
|
||||
this.displayJson(result);
|
||||
break;
|
||||
|
||||
case 'text':
|
||||
default:
|
||||
this.displayText(result);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display in JSON format
|
||||
*/
|
||||
private displayJson(result: NextTaskResult): void {
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display in text format
|
||||
*/
|
||||
private displayText(result: NextTaskResult): void {
|
||||
// Display header with tag (no file path for next command)
|
||||
displayHeader({
|
||||
tag: result.tag || 'master'
|
||||
});
|
||||
|
||||
if (!result.found || !result.task) {
|
||||
// No next task available
|
||||
console.log(
|
||||
boxen(
|
||||
chalk.yellow(
|
||||
'No tasks available to work on. All tasks are either completed, blocked by dependencies, or in progress.'
|
||||
),
|
||||
{
|
||||
padding: 1,
|
||||
borderStyle: 'round',
|
||||
borderColor: 'yellow',
|
||||
title: '⚠ NO TASKS AVAILABLE ⚠',
|
||||
titleAlignment: 'center'
|
||||
}
|
||||
)
|
||||
);
|
||||
console.log(`\n${chalk.gray('Storage: ' + result.storageType)}`);
|
||||
console.log(
|
||||
`\n${chalk.dim('Tip: Try')} ${chalk.cyan('task-master list --status pending')} ${chalk.dim('to see all pending tasks')}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const task = result.task;
|
||||
|
||||
// Display the task details using the same component as 'show' command
|
||||
// with a custom header indicating this is the next task
|
||||
const customHeader = `Next Task: #${task.id} - ${task.title}`;
|
||||
displayTaskDetails(task, {
|
||||
customHeader,
|
||||
headerColor: 'green',
|
||||
showSuggestedActions: true
|
||||
});
|
||||
|
||||
console.log(`\n${chalk.gray('Storage: ' + result.storageType)}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the last result for programmatic access
|
||||
*/
|
||||
private setLastResult(result: NextTaskResult): void {
|
||||
this.lastResult = result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last result (for programmatic usage)
|
||||
*/
|
||||
getLastResult(): NextTaskResult | undefined {
|
||||
return this.lastResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up resources
|
||||
*/
|
||||
async cleanup(): Promise<void> {
|
||||
if (this.tmCore) {
|
||||
await this.tmCore.close();
|
||||
this.tmCore = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register this command on an existing program
|
||||
*/
|
||||
static register(program: Command, name?: string): NextCommand {
|
||||
const nextCommand = new NextCommand(name);
|
||||
program.addCommand(nextCommand);
|
||||
return nextCommand;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
// Commands
|
||||
export { ListTasksCommand } from './commands/list.command.js';
|
||||
export { ShowCommand } from './commands/show.command.js';
|
||||
export { NextCommand } from './commands/next.command.js';
|
||||
export { AuthCommand } from './commands/auth.command.js';
|
||||
export { ContextCommand } from './commands/context.command.js';
|
||||
export { StartCommand } from './commands/start.command.js';
|
||||
|
||||
@@ -25,9 +25,9 @@ export function displayHeader(options: HeaderOptions = {}): void {
|
||||
let tagInfo = '';
|
||||
|
||||
if (tag && tag !== 'master') {
|
||||
tagInfo = `🏷 tag: ${chalk.cyan(tag)}`;
|
||||
tagInfo = `🏷 tag: ${chalk.cyan(tag)}`;
|
||||
} else {
|
||||
tagInfo = `🏷 tag: ${chalk.cyan('master')}`;
|
||||
tagInfo = `🏷 tag: ${chalk.cyan('master')}`;
|
||||
}
|
||||
|
||||
console.log(tagInfo);
|
||||
@@ -39,7 +39,5 @@ export function displayHeader(options: HeaderOptions = {}): void {
|
||||
: `${process.cwd()}/${filePath}`;
|
||||
console.log(`Listing tasks from: ${chalk.dim(absolutePath)}`);
|
||||
}
|
||||
|
||||
console.log(); // Empty line for spacing
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2441,57 +2441,6 @@ ${result.result}
|
||||
}
|
||||
});
|
||||
|
||||
// next command
|
||||
programInstance
|
||||
.command('next')
|
||||
.description(
|
||||
`Show the next task to work on based on dependencies and status${chalk.reset('')}`
|
||||
)
|
||||
.option(
|
||||
'-f, --file <file>',
|
||||
'Path to the tasks file',
|
||||
TASKMASTER_TASKS_FILE
|
||||
)
|
||||
.option(
|
||||
'-r, --report <report>',
|
||||
'Path to the complexity report file',
|
||||
COMPLEXITY_REPORT_FILE
|
||||
)
|
||||
.option('--tag <tag>', 'Specify tag context for task operations')
|
||||
.action(async (options) => {
|
||||
const initOptions = {
|
||||
tasksPath: options.file || true,
|
||||
tag: options.tag
|
||||
};
|
||||
|
||||
if (options.report && options.report !== COMPLEXITY_REPORT_FILE) {
|
||||
initOptions.complexityReportPath = options.report;
|
||||
}
|
||||
|
||||
// Initialize TaskMaster
|
||||
const taskMaster = initTaskMaster({
|
||||
tasksPath: options.file || true,
|
||||
tag: options.tag,
|
||||
complexityReportPath: options.report || false
|
||||
});
|
||||
|
||||
const tag = taskMaster.getCurrentTag();
|
||||
|
||||
const context = {
|
||||
projectRoot: taskMaster.getProjectRoot(),
|
||||
tag
|
||||
};
|
||||
|
||||
// Show current tag context
|
||||
displayCurrentTagIndicator(tag);
|
||||
|
||||
await displayNextTask(
|
||||
taskMaster.getTasksPath(),
|
||||
taskMaster.getComplexityReportPath(),
|
||||
context
|
||||
);
|
||||
});
|
||||
|
||||
// add-dependency command
|
||||
programInstance
|
||||
.command('add-dependency')
|
||||
|
||||
@@ -307,6 +307,20 @@ function validateProviderModelCombination(providerName, modelId) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of supported model IDs for a given provider from supported-models.json
|
||||
* @param {string} providerName - The name of the provider (e.g., 'claude-code', 'anthropic')
|
||||
* @returns {string[]} Array of supported model IDs, or empty array if provider not found
|
||||
*/
|
||||
export function getSupportedModelsForProvider(providerName) {
|
||||
if (!MODEL_MAP[providerName]) {
|
||||
return [];
|
||||
}
|
||||
return MODEL_MAP[providerName]
|
||||
.filter((model) => model.supported !== false)
|
||||
.map((model) => model.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates Claude Code AI provider custom settings
|
||||
* @param {object} settings The settings to validate
|
||||
|
||||
@@ -43,6 +43,28 @@
|
||||
"allowed_roles": ["main", "fallback"],
|
||||
"max_tokens": 8192,
|
||||
"supported": true
|
||||
},
|
||||
{
|
||||
"id": "claude-sonnet-4-5-20250929",
|
||||
"swe_score": 0.73,
|
||||
"cost_per_1m_tokens": {
|
||||
"input": 3.0,
|
||||
"output": 15.0
|
||||
},
|
||||
"allowed_roles": ["main", "fallback"],
|
||||
"max_tokens": 64000,
|
||||
"supported": true
|
||||
},
|
||||
{
|
||||
"id": "claude-haiku-4-5-20251001",
|
||||
"swe_score": 0.45,
|
||||
"cost_per_1m_tokens": {
|
||||
"input": 1.0,
|
||||
"output": 5.0
|
||||
},
|
||||
"allowed_roles": ["main", "fallback"],
|
||||
"max_tokens": 200000,
|
||||
"supported": true
|
||||
}
|
||||
],
|
||||
"claude-code": [
|
||||
@@ -67,6 +89,17 @@
|
||||
"allowed_roles": ["main", "fallback", "research"],
|
||||
"max_tokens": 64000,
|
||||
"supported": true
|
||||
},
|
||||
{
|
||||
"id": "haiku",
|
||||
"swe_score": 0.45,
|
||||
"cost_per_1m_tokens": {
|
||||
"input": 0,
|
||||
"output": 0
|
||||
},
|
||||
"allowed_roles": ["main", "fallback", "research"],
|
||||
"max_tokens": 200000,
|
||||
"supported": true
|
||||
}
|
||||
],
|
||||
"codex-cli": [
|
||||
|
||||
@@ -12,7 +12,10 @@
|
||||
|
||||
import { createClaudeCode } from 'ai-sdk-provider-claude-code';
|
||||
import { BaseAIProvider } from './base-provider.js';
|
||||
import { getClaudeCodeSettingsForCommand } from '../../scripts/modules/config-manager.js';
|
||||
import {
|
||||
getClaudeCodeSettingsForCommand,
|
||||
getSupportedModelsForProvider
|
||||
} from '../../scripts/modules/config-manager.js';
|
||||
import { execSync } from 'child_process';
|
||||
import { log } from '../../scripts/modules/utils.js';
|
||||
|
||||
@@ -24,14 +27,24 @@ let _claudeCliAvailable = null;
|
||||
*
|
||||
* Features:
|
||||
* - No API key required (uses local Claude Code CLI)
|
||||
* - Supports 'sonnet' and 'opus' models
|
||||
* - Supported models loaded from supported-models.json
|
||||
* - Command-specific configuration support
|
||||
*/
|
||||
export class ClaudeCodeProvider extends BaseAIProvider {
|
||||
constructor() {
|
||||
super();
|
||||
this.name = 'Claude Code';
|
||||
this.supportedModels = ['sonnet', 'opus'];
|
||||
// Load supported models from supported-models.json
|
||||
this.supportedModels = getSupportedModelsForProvider('claude-code');
|
||||
|
||||
// Validate that models were loaded successfully
|
||||
if (this.supportedModels.length === 0) {
|
||||
log(
|
||||
'warn',
|
||||
'No supported models found for claude-code provider. Check supported-models.json configuration.'
|
||||
);
|
||||
}
|
||||
|
||||
// Claude Code requires explicit JSON schema mode
|
||||
this.needsExplicitJsonSchema = true;
|
||||
// Claude Code does not support temperature parameter
|
||||
|
||||
@@ -10,7 +10,10 @@ import { createCodexCli } from 'ai-sdk-provider-codex-cli';
|
||||
import { BaseAIProvider } from './base-provider.js';
|
||||
import { execSync } from 'child_process';
|
||||
import { log } from '../../scripts/modules/utils.js';
|
||||
import { getCodexCliSettingsForCommand } from '../../scripts/modules/config-manager.js';
|
||||
import {
|
||||
getCodexCliSettingsForCommand,
|
||||
getSupportedModelsForProvider
|
||||
} from '../../scripts/modules/config-manager.js';
|
||||
|
||||
export class CodexCliProvider extends BaseAIProvider {
|
||||
constructor() {
|
||||
@@ -20,8 +23,17 @@ export class CodexCliProvider extends BaseAIProvider {
|
||||
this.needsExplicitJsonSchema = false;
|
||||
// Codex CLI does not support temperature parameter
|
||||
this.supportsTemperature = false;
|
||||
// Restrict to supported models for OAuth subscription usage
|
||||
this.supportedModels = ['gpt-5', 'gpt-5-codex'];
|
||||
// Load supported models from supported-models.json
|
||||
this.supportedModels = getSupportedModelsForProvider('codex-cli');
|
||||
|
||||
// Validate that models were loaded successfully
|
||||
if (this.supportedModels.length === 0) {
|
||||
log(
|
||||
'warn',
|
||||
'No supported models found for codex-cli provider. Check supported-models.json configuration.'
|
||||
);
|
||||
}
|
||||
|
||||
// CLI availability check cache
|
||||
this._codexCliChecked = false;
|
||||
this._codexCliAvailable = null;
|
||||
|
||||
@@ -43,9 +43,9 @@ describe('Claude Code Error Handling', () => {
|
||||
|
||||
// These should work even if CLI is not available
|
||||
expect(provider.name).toBe('Claude Code');
|
||||
expect(provider.getSupportedModels()).toEqual(['sonnet', 'opus']);
|
||||
expect(provider.getSupportedModels()).toEqual(['opus', 'sonnet', 'haiku']);
|
||||
expect(provider.isModelSupported('sonnet')).toBe(true);
|
||||
expect(provider.isModelSupported('haiku')).toBe(false);
|
||||
expect(provider.isModelSupported('haiku')).toBe(true);
|
||||
expect(provider.isRequiredApiKey()).toBe(false);
|
||||
expect(() => provider.validateAuth()).not.toThrow();
|
||||
});
|
||||
|
||||
@@ -40,14 +40,14 @@ describe('Claude Code Integration (Optional)', () => {
|
||||
it('should create a working provider instance', () => {
|
||||
const provider = new ClaudeCodeProvider();
|
||||
expect(provider.name).toBe('Claude Code');
|
||||
expect(provider.getSupportedModels()).toEqual(['sonnet', 'opus']);
|
||||
expect(provider.getSupportedModels()).toEqual(['opus', 'sonnet', 'haiku']);
|
||||
});
|
||||
|
||||
it('should support model validation', () => {
|
||||
const provider = new ClaudeCodeProvider();
|
||||
expect(provider.isModelSupported('sonnet')).toBe(true);
|
||||
expect(provider.isModelSupported('opus')).toBe(true);
|
||||
expect(provider.isModelSupported('haiku')).toBe(false);
|
||||
expect(provider.isModelSupported('haiku')).toBe(true);
|
||||
expect(provider.isModelSupported('unknown')).toBe(false);
|
||||
});
|
||||
|
||||
|
||||
@@ -28,6 +28,14 @@ jest.unstable_mockModule('../../../src/ai-providers/base-provider.js', () => ({
|
||||
}
|
||||
}));
|
||||
|
||||
// Mock config getters
|
||||
jest.unstable_mockModule('../../../scripts/modules/config-manager.js', () => ({
|
||||
getClaudeCodeSettingsForCommand: jest.fn(() => ({})),
|
||||
getSupportedModelsForProvider: jest.fn(() => ['opus', 'sonnet', 'haiku']),
|
||||
getDebugFlag: jest.fn(() => false),
|
||||
getLogLevel: jest.fn(() => 'info')
|
||||
}));
|
||||
|
||||
// Import after mocking
|
||||
const { ClaudeCodeProvider } = await import(
|
||||
'../../../src/ai-providers/claude-code.js'
|
||||
@@ -96,13 +104,13 @@ describe('ClaudeCodeProvider', () => {
|
||||
describe('model support', () => {
|
||||
it('should return supported models', () => {
|
||||
const models = provider.getSupportedModels();
|
||||
expect(models).toEqual(['sonnet', 'opus']);
|
||||
expect(models).toEqual(['opus', 'sonnet', 'haiku']);
|
||||
});
|
||||
|
||||
it('should check if model is supported', () => {
|
||||
expect(provider.isModelSupported('sonnet')).toBe(true);
|
||||
expect(provider.isModelSupported('opus')).toBe(true);
|
||||
expect(provider.isModelSupported('haiku')).toBe(false);
|
||||
expect(provider.isModelSupported('haiku')).toBe(true);
|
||||
expect(provider.isModelSupported('unknown')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,6 +20,7 @@ jest.unstable_mockModule('ai-sdk-provider-codex-cli', () => ({
|
||||
// Mock config getters
|
||||
jest.unstable_mockModule('../../../scripts/modules/config-manager.js', () => ({
|
||||
getCodexCliSettingsForCommand: jest.fn(() => ({ allowNpx: true })),
|
||||
getSupportedModelsForProvider: jest.fn(() => ['gpt-5', 'gpt-5-codex']),
|
||||
// Provide commonly imported getters to satisfy other module imports if any
|
||||
getDebugFlag: jest.fn(() => false),
|
||||
getLogLevel: jest.fn(() => 'info')
|
||||
|
||||
Reference in New Issue
Block a user