Compare commits
10 Commits
ralph/fix/
...
docs/auto-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1eb84f9660 | ||
|
|
7b5a7c4495 | ||
|
|
caee040907 | ||
|
|
4b5473860b | ||
|
|
b43b7ce201 | ||
|
|
86027f1ee4 | ||
|
|
4f984f8a69 | ||
|
|
f7646f41b5 | ||
|
|
20004a39ea | ||
|
|
f1393f47b1 |
11
.changeset/brave-lions-sing.md
Normal file
11
.changeset/brave-lions-sing.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
"task-master-ai": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add Codex CLI provider with OAuth authentication
|
||||||
|
|
||||||
|
- Added codex-cli provider for GPT-5 and GPT-5-Codex models (272K input / 128K output)
|
||||||
|
- OAuth-first authentication via `codex login` - no API key required
|
||||||
|
- Optional OPENAI_CODEX_API_KEY support
|
||||||
|
- Codebase analysis capabilities automatically enabled
|
||||||
|
- Command-specific settings and approval/sandbox modes
|
||||||
5
.changeset/easy-spiders-wave.md
Normal file
5
.changeset/easy-spiders-wave.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"task-master-ai": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Do a quick fix on build
|
||||||
5
.changeset/fix-mcp-connection-errors.md
Normal file
5
.changeset/fix-mcp-connection-errors.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"task-master-ai": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix MCP connection errors caused by deprecated generateTaskFiles calls. Resolves "Cannot read properties of null (reading 'toString')" errors when using MCP tools for task management operations.
|
||||||
5
.changeset/fix-mcp-default-tasks-path.md
Normal file
5
.changeset/fix-mcp-default-tasks-path.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"task-master-ai": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix MCP server error when file parameter not provided - now properly constructs default tasks.json path instead of failing with 'tasksJsonPath is required' error.
|
||||||
23
.changeset/pre.json
Normal file
23
.changeset/pre.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"mode": "pre",
|
||||||
|
"tag": "rc",
|
||||||
|
"initialVersions": {
|
||||||
|
"task-master-ai": "0.27.3",
|
||||||
|
"docs": "0.0.4",
|
||||||
|
"extension": "0.25.4"
|
||||||
|
},
|
||||||
|
"changesets": [
|
||||||
|
"chore-fix-docs",
|
||||||
|
"cursor-slash-commands",
|
||||||
|
"curvy-weeks-flow",
|
||||||
|
"easy-spiders-wave",
|
||||||
|
"flat-cities-say",
|
||||||
|
"forty-tables-invite",
|
||||||
|
"gentle-cats-dance",
|
||||||
|
"mcp-timeout-configuration",
|
||||||
|
"petite-ideas-grab",
|
||||||
|
"silly-pandas-find",
|
||||||
|
"sweet-maps-rule",
|
||||||
|
"whole-pigs-say"
|
||||||
|
]
|
||||||
|
}
|
||||||
8
.changeset/whole-pigs-say.md
Normal file
8
.changeset/whole-pigs-say.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
"task-master-ai": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix complexity score not showing for `task-master show` and `task-master list`
|
||||||
|
|
||||||
|
- Added complexity score on "next task" when running `task-master list`
|
||||||
|
- Added colors to complexity to reflect complexity (easy, medium, hard)
|
||||||
72
CHANGELOG.md
72
CHANGELOG.md
@@ -1,5 +1,77 @@
|
|||||||
# task-master-ai
|
# task-master-ai
|
||||||
|
|
||||||
|
## 0.28.0-rc.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#1274](https://github.com/eyaltoledano/claude-task-master/pull/1274) [`4f984f8`](https://github.com/eyaltoledano/claude-task-master/commit/4f984f8a6965da9f9c7edd60ddfd6560ac022917) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Do a quick fix on build
|
||||||
|
|
||||||
|
## 0.28.0-rc.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- [#1215](https://github.com/eyaltoledano/claude-task-master/pull/1215) [`0079b7d`](https://github.com/eyaltoledano/claude-task-master/commit/0079b7defdad550811f704c470fdd01955d91d4d) Thanks [@joedanz](https://github.com/joedanz)! - 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.
|
||||||
|
|
||||||
|
- [#1246](https://github.com/eyaltoledano/claude-task-master/pull/1246) [`18aa416`](https://github.com/eyaltoledano/claude-task-master/commit/18aa416035f44345bde1c7321490345733a5d042) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Added api keys page on docs website: docs.task-master.dev/getting-started/api-keys
|
||||||
|
|
||||||
|
- [#1246](https://github.com/eyaltoledano/claude-task-master/pull/1246) [`18aa416`](https://github.com/eyaltoledano/claude-task-master/commit/18aa416035f44345bde1c7321490345733a5d042) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - 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
|
||||||
|
|
||||||
|
- [#1262](https://github.com/eyaltoledano/claude-task-master/pull/1262) [`738ec51`](https://github.com/eyaltoledano/claude-task-master/commit/738ec51c049a295a12839b2dfddaf05e23b8fede) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Migrate AI services to use generateObject for structured data generation
|
||||||
|
|
||||||
|
This update migrates all AI service calls from generateText to generateObject, ensuring more reliable and structured responses across all commands.
|
||||||
|
|
||||||
|
### Key Changes:
|
||||||
|
- **Unified AI Service**: Replaced separate generateText implementations with a single generateObjectService that handles structured data generation
|
||||||
|
- **JSON Mode Support**: Added proper JSON mode configuration for providers that support it (OpenAI, Anthropic, Google, Groq)
|
||||||
|
- **Schema Validation**: Integrated Zod schemas for all AI-generated content with automatic validation
|
||||||
|
- **Provider Compatibility**: Maintained compatibility with all existing providers while leveraging their native structured output capabilities
|
||||||
|
- **Improved Reliability**: Structured output generation reduces parsing errors and ensures consistent data formats
|
||||||
|
|
||||||
|
### Technical Improvements:
|
||||||
|
- Centralized provider configuration in `ai-providers-unified.js`
|
||||||
|
- Added `generateObject` support detection for each provider
|
||||||
|
- Implemented proper error handling for schema validation failures
|
||||||
|
- Maintained backward compatibility with existing prompt structures
|
||||||
|
|
||||||
|
### Bug Fixes:
|
||||||
|
- Fixed subtask ID numbering issue where AI was generating inconsistent IDs (101-105, 601-603) instead of sequential numbering (1, 2, 3...)
|
||||||
|
- Enhanced prompt instructions to enforce proper ID generation patterns
|
||||||
|
- Ensured subtasks display correctly as X.1, X.2, X.3 format
|
||||||
|
|
||||||
|
This migration improves the reliability and consistency of AI-generated content throughout the Task Master application.
|
||||||
|
|
||||||
|
- [#1112](https://github.com/eyaltoledano/claude-task-master/pull/1112) [`d67b81d`](https://github.com/eyaltoledano/claude-task-master/commit/d67b81d25ddd927fabb6f5deb368e8993519c541) Thanks [@olssonsten](https://github.com/olssonsten)! - 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.
|
||||||
|
|
||||||
|
- [#1246](https://github.com/eyaltoledano/claude-task-master/pull/1246) [`986ac11`](https://github.com/eyaltoledano/claude-task-master/commit/986ac117aee00bcd3e6830a0f76e1ad6d10e0bca) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Upgrade grok-cli ai provider to ai sdk v5
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#1235](https://github.com/eyaltoledano/claude-task-master/pull/1235) [`aaacc3d`](https://github.com/eyaltoledano/claude-task-master/commit/aaacc3dae36247b4de72b2d2697f49e5df6d01e3) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Improve `analyze-complexity` cli docs and `--research` flag documentation
|
||||||
|
|
||||||
|
- [#1251](https://github.com/eyaltoledano/claude-task-master/pull/1251) [`0b2c696`](https://github.com/eyaltoledano/claude-task-master/commit/0b2c6967c4605c33a100cff16f6ce8ff09ad06f0) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Change parent task back to "pending" when all subtasks are in "pending" state
|
||||||
|
|
||||||
|
- [#1172](https://github.com/eyaltoledano/claude-task-master/pull/1172) [`b5fe723`](https://github.com/eyaltoledano/claude-task-master/commit/b5fe723f8ead928e9f2dbde13b833ee70ac3382d) Thanks [@jujax](https://github.com/jujax)! - Fix Claude Code settings validation for pathToClaudeCodeExecutable
|
||||||
|
|
||||||
|
- [#1192](https://github.com/eyaltoledano/claude-task-master/pull/1192) [`2b69936`](https://github.com/eyaltoledano/claude-task-master/commit/2b69936ee7b34346d6de5175af20e077359e2e2a) Thanks [@nukunga](https://github.com/nukunga)! - Fix sonar deep research model failing, should be called `sonar-deep-research`
|
||||||
|
|
||||||
|
- [#1270](https://github.com/eyaltoledano/claude-task-master/pull/1270) [`20004a3`](https://github.com/eyaltoledano/claude-task-master/commit/20004a39ea848f747e1ff48981bfe176554e4055) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fix complexity score not showing for `task-master show` and `task-master list`
|
||||||
|
- Added complexity score on "next task" when running `task-master list`
|
||||||
|
- Added colors to complexity to reflect complexity (easy, medium, hard)
|
||||||
|
|
||||||
## 0.27.3
|
## 0.27.3
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
22
CLAUDE.md
22
CLAUDE.md
@@ -4,6 +4,28 @@
|
|||||||
**Import Task Master's development workflow commands and guidelines, treat as if import is in the main CLAUDE.md file.**
|
**Import Task Master's development workflow commands and guidelines, treat as if import is in the main CLAUDE.md file.**
|
||||||
@./.taskmaster/CLAUDE.md
|
@./.taskmaster/CLAUDE.md
|
||||||
|
|
||||||
|
## Test Guidelines
|
||||||
|
|
||||||
|
### Synchronous Tests
|
||||||
|
- **NEVER use async/await in test functions** unless testing actual asynchronous operations
|
||||||
|
- Use synchronous top-level imports instead of dynamic `await import()`
|
||||||
|
- Test bodies should be synchronous whenever possible
|
||||||
|
- Example:
|
||||||
|
```javascript
|
||||||
|
// ✅ CORRECT - Synchronous imports
|
||||||
|
import { MyClass } from '../src/my-class.js';
|
||||||
|
|
||||||
|
it('should verify behavior', () => {
|
||||||
|
expect(new MyClass().property).toBe(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ❌ INCORRECT - Async imports
|
||||||
|
it('should verify behavior', async () => {
|
||||||
|
const { MyClass } = await import('../src/my-class.js');
|
||||||
|
expect(new MyClass().property).toBe(value);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
## Changeset Guidelines
|
## Changeset Guidelines
|
||||||
|
|
||||||
- When creating changesets, remember that it's user-facing, meaning we don't have to get into the specifics of the code, but rather mention what the end-user is getting or fixing from this changeset.
|
- When creating changesets, remember that it's user-facing, meaning we don't have to get into the specifics of the code, but rather mention what the end-user is getting or fixing from this changeset.
|
||||||
@@ -88,8 +88,9 @@ At least one (1) of the following is required:
|
|||||||
- xAI API Key (for research or main model)
|
- xAI API Key (for research or main model)
|
||||||
- OpenRouter API Key (for research or main model)
|
- OpenRouter API Key (for research or main model)
|
||||||
- Claude Code (no API key required - requires Claude Code CLI)
|
- Claude Code (no API key required - requires Claude Code CLI)
|
||||||
|
- Codex CLI (OAuth via ChatGPT subscription - requires Codex CLI)
|
||||||
|
|
||||||
Using the research model is optional but highly recommended. You will need at least ONE API key (unless using Claude Code). Adding all API keys enables you to seamlessly switch between model providers at will.
|
Using the research model is optional but highly recommended. You will need at least ONE API key (unless using Claude Code or Codex CLI with OAuth). Adding all API keys enables you to seamlessly switch between model providers at will.
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
|
|||||||
@@ -281,9 +281,14 @@ export class ListTasksCommand extends Command {
|
|||||||
const priorityBreakdown = getPriorityBreakdown(tasks);
|
const priorityBreakdown = getPriorityBreakdown(tasks);
|
||||||
|
|
||||||
// Find next task following the same logic as findNextTask
|
// Find next task following the same logic as findNextTask
|
||||||
const nextTask = this.findNextTask(tasks);
|
const nextTaskInfo = this.findNextTask(tasks);
|
||||||
|
|
||||||
// Display dashboard boxes
|
// Get the full task object with complexity data already included
|
||||||
|
const nextTask = nextTaskInfo
|
||||||
|
? tasks.find((t) => String(t.id) === String(nextTaskInfo.id))
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
// Display dashboard boxes (nextTask already has complexity from storage enrichment)
|
||||||
displayDashboards(
|
displayDashboards(
|
||||||
taskStats,
|
taskStats,
|
||||||
subtaskStats,
|
subtaskStats,
|
||||||
@@ -303,14 +308,16 @@ export class ListTasksCommand extends Command {
|
|||||||
|
|
||||||
// Display recommended next task section immediately after table
|
// Display recommended next task section immediately after table
|
||||||
if (nextTask) {
|
if (nextTask) {
|
||||||
// Find the full task object to get description
|
const description = getTaskDescription(nextTask);
|
||||||
const fullTask = tasks.find((t) => String(t.id) === String(nextTask.id));
|
|
||||||
const description = fullTask ? getTaskDescription(fullTask) : undefined;
|
|
||||||
|
|
||||||
displayRecommendedNextTask({
|
displayRecommendedNextTask({
|
||||||
...nextTask,
|
id: nextTask.id,
|
||||||
status: 'pending', // Next task is typically pending
|
title: nextTask.title,
|
||||||
description
|
priority: nextTask.priority,
|
||||||
|
status: nextTask.status,
|
||||||
|
dependencies: nextTask.dependencies,
|
||||||
|
description,
|
||||||
|
complexity: nextTask.complexity as number | undefined
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
displayRecommendedNextTask(undefined);
|
displayRecommendedNextTask(undefined);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import boxen from 'boxen';
|
import boxen from 'boxen';
|
||||||
import type { Task, TaskPriority } from '@tm/core/types';
|
import type { Task, TaskPriority } from '@tm/core/types';
|
||||||
|
import { getComplexityWithColor } from '../../utils/ui.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Statistics for task collection
|
* Statistics for task collection
|
||||||
@@ -479,7 +480,7 @@ export function displayDependencyDashboard(
|
|||||||
? chalk.cyan(nextTask.dependencies.join(', '))
|
? chalk.cyan(nextTask.dependencies.join(', '))
|
||||||
: chalk.gray('None')
|
: chalk.gray('None')
|
||||||
}\n` +
|
}\n` +
|
||||||
`Complexity: ${nextTask?.complexity || chalk.gray('N/A')}`;
|
`Complexity: ${nextTask?.complexity !== undefined ? getComplexityWithColor(nextTask.complexity) : chalk.gray('N/A')}`;
|
||||||
|
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import boxen from 'boxen';
|
import boxen from 'boxen';
|
||||||
import type { Task } from '@tm/core/types';
|
import type { Task } from '@tm/core/types';
|
||||||
|
import { getComplexityWithColor } from '../../utils/ui.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Next task display options
|
* Next task display options
|
||||||
@@ -17,6 +18,7 @@ export interface NextTaskDisplayOptions {
|
|||||||
status?: string;
|
status?: string;
|
||||||
dependencies?: (string | number)[];
|
dependencies?: (string | number)[];
|
||||||
description?: string;
|
description?: string;
|
||||||
|
complexity?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -82,6 +84,11 @@ export function displayRecommendedNextTask(
|
|||||||
: chalk.cyan(task.dependencies.join(', '));
|
: chalk.cyan(task.dependencies.join(', '));
|
||||||
content.push(`Dependencies: ${depsDisplay}`);
|
content.push(`Dependencies: ${depsDisplay}`);
|
||||||
|
|
||||||
|
// Complexity with color and label
|
||||||
|
if (typeof task.complexity === 'number') {
|
||||||
|
content.push(`Complexity: ${getComplexityWithColor(task.complexity)}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Description if available
|
// Description if available
|
||||||
if (task.description) {
|
if (task.description) {
|
||||||
content.push('');
|
content.push('');
|
||||||
|
|||||||
@@ -9,7 +9,11 @@ import Table from 'cli-table3';
|
|||||||
import { marked, MarkedExtension } from 'marked';
|
import { marked, MarkedExtension } from 'marked';
|
||||||
import { markedTerminal } from 'marked-terminal';
|
import { markedTerminal } from 'marked-terminal';
|
||||||
import type { Task } from '@tm/core/types';
|
import type { Task } from '@tm/core/types';
|
||||||
import { getStatusWithColor, getPriorityWithColor } from '../../utils/ui.js';
|
import {
|
||||||
|
getStatusWithColor,
|
||||||
|
getPriorityWithColor,
|
||||||
|
getComplexityWithColor
|
||||||
|
} from '../../utils/ui.js';
|
||||||
|
|
||||||
// Configure marked to use terminal renderer with subtle colors
|
// Configure marked to use terminal renderer with subtle colors
|
||||||
marked.use(
|
marked.use(
|
||||||
@@ -108,7 +112,9 @@ export function displayTaskProperties(task: Task): void {
|
|||||||
getStatusWithColor(task.status),
|
getStatusWithColor(task.status),
|
||||||
getPriorityWithColor(task.priority),
|
getPriorityWithColor(task.priority),
|
||||||
deps,
|
deps,
|
||||||
'N/A',
|
typeof task.complexity === 'number'
|
||||||
|
? getComplexityWithColor(task.complexity)
|
||||||
|
: chalk.gray('N/A'),
|
||||||
task.description || ''
|
task.description || ''
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
|
|||||||
@@ -84,7 +84,23 @@ export function getPriorityWithColor(priority: TaskPriority): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get colored complexity display
|
* Get complexity color and label based on score thresholds
|
||||||
|
*/
|
||||||
|
function getComplexityLevel(score: number): {
|
||||||
|
color: (text: string) => string;
|
||||||
|
label: string;
|
||||||
|
} {
|
||||||
|
if (score >= 7) {
|
||||||
|
return { color: chalk.hex('#CC0000'), label: 'High' };
|
||||||
|
} else if (score >= 4) {
|
||||||
|
return { color: chalk.hex('#FF8800'), label: 'Medium' };
|
||||||
|
} else {
|
||||||
|
return { color: chalk.green, label: 'Low' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get colored complexity display with dot indicator (simple format)
|
||||||
*/
|
*/
|
||||||
export function getComplexityWithColor(complexity: number | string): string {
|
export function getComplexityWithColor(complexity: number | string): string {
|
||||||
const score =
|
const score =
|
||||||
@@ -94,13 +110,20 @@ export function getComplexityWithColor(complexity: number | string): string {
|
|||||||
return chalk.gray('N/A');
|
return chalk.gray('N/A');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (score >= 8) {
|
const { color } = getComplexityLevel(score);
|
||||||
return chalk.red.bold(`${score} (High)`);
|
return color(`● ${score}`);
|
||||||
} else if (score >= 5) {
|
}
|
||||||
return chalk.yellow(`${score} (Medium)`);
|
|
||||||
} else {
|
/**
|
||||||
return chalk.green(`${score} (Low)`);
|
* Get colored complexity display with /10 format (for dashboards)
|
||||||
|
*/
|
||||||
|
export function getComplexityWithScore(complexity: number | undefined): string {
|
||||||
|
if (typeof complexity !== 'number') {
|
||||||
|
return chalk.gray('N/A');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { color, label } = getComplexityLevel(complexity);
|
||||||
|
return color(`${complexity}/10 (${label})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -323,8 +346,12 @@ export function createTaskTable(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (showComplexity) {
|
if (showComplexity) {
|
||||||
// Show N/A if no complexity score
|
// Show complexity score from report if available
|
||||||
row.push(chalk.gray('N/A'));
|
if (typeof task.complexity === 'number') {
|
||||||
|
row.push(getComplexityWithColor(task.complexity));
|
||||||
|
} else {
|
||||||
|
row.push(chalk.gray('N/A'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
table.push(row);
|
table.push(row);
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ The CLI is organized into a series of commands, each with its own set of options
|
|||||||
### 4. Project and Configuration
|
### 4. Project and Configuration
|
||||||
|
|
||||||
- **`init`**: Initializes a new project.
|
- **`init`**: Initializes a new project.
|
||||||
- **`generate`**: Generates individual task files.
|
- **`generate`**: Generates individual task files from tasks.json. Run this manually after task operations to create readable text files for each task.
|
||||||
- **`migrate`**: Migrates a project to the new directory structure.
|
- **`migrate`**: Migrates a project to the new directory structure.
|
||||||
- **`research`**: Performs AI-powered research.
|
- **`research`**: Performs AI-powered research.
|
||||||
- `--query <query>`: The research query.
|
- `--query <query>`: The research query.
|
||||||
@@ -123,7 +123,7 @@ The core functionalities can be categorized as follows:
|
|||||||
|
|
||||||
### 1. Task and Subtask Management
|
### 1. Task and Subtask Management
|
||||||
|
|
||||||
These functions are the bread and butter of the application, allowing for the creation, modification, and deletion of tasks and subtasks.
|
These functions are the bread and butter of the application, allowing for the creation, modification, and deletion of tasks and subtasks. Note: As of v0.27.3, these operations no longer automatically generate individual task files - use the `generate` command manually when needed.
|
||||||
|
|
||||||
- **`addTask(prompt, dependencies, priority)`**: Creates a new task using an AI-powered prompt to generate the title, description, details, and test strategy. It can also be used to create a task manually by providing the task data directly.
|
- **`addTask(prompt, dependencies, priority)`**: Creates a new task using an AI-powered prompt to generate the title, description, details, and test strategy. It can also be used to create a task manually by providing the task data directly.
|
||||||
- **`addSubtask(parentId, existingTaskId, newSubtaskData)`**: Adds a subtask to a parent task. It can either convert an existing task into a subtask or create a new subtask from scratch.
|
- **`addSubtask(parentId, existingTaskId, newSubtaskData)`**: Adds a subtask to a parent task. It can either convert an existing task into a subtask or create a new subtask from scratch.
|
||||||
@@ -167,7 +167,7 @@ These functions are crucial for managing the relationships between tasks.
|
|||||||
|
|
||||||
These functions are for managing the project and its configuration.
|
These functions are for managing the project and its configuration.
|
||||||
|
|
||||||
- **`generateTaskFiles()`**: Generates individual task files from `tasks.json`.
|
- **`generateTaskFiles()`**: Generates individual task files from `tasks.json`. This is now a manual operation - task management operations no longer automatically generate these files.
|
||||||
- **`migrateProject()`**: Migrates the project to the new `.taskmaster` directory structure.
|
- **`migrateProject()`**: Migrates the project to the new `.taskmaster` directory structure.
|
||||||
- **`performResearch(query, options)`**: Performs AI-powered research with project context.
|
- **`performResearch(query, options)`**: Performs AI-powered research with project context.
|
||||||
|
|
||||||
@@ -225,7 +225,7 @@ The MCP tools can be categorized in the same way as the core functionalities:
|
|||||||
### 5. Project and Configuration
|
### 5. Project and Configuration
|
||||||
|
|
||||||
- **`initialize_project`**: Initializes a new project.
|
- **`initialize_project`**: Initializes a new project.
|
||||||
- **`generate`**: Generates individual task files.
|
- **`generate`**: Generates individual task files from tasks.json. Run this manually when you want to create readable text files for each task.
|
||||||
- **`models`**: Manages AI model configurations.
|
- **`models`**: Manages AI model configurations.
|
||||||
- **`research`**: Performs AI-powered research.
|
- **`research`**: Performs AI-powered research.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
# Change Log
|
# Change Log
|
||||||
|
|
||||||
|
## 0.25.5-rc.0
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [[`aaacc3d`](https://github.com/eyaltoledano/claude-task-master/commit/aaacc3dae36247b4de72b2d2697f49e5df6d01e3), [`0079b7d`](https://github.com/eyaltoledano/claude-task-master/commit/0079b7defdad550811f704c470fdd01955d91d4d), [`0b2c696`](https://github.com/eyaltoledano/claude-task-master/commit/0b2c6967c4605c33a100cff16f6ce8ff09ad06f0), [`18aa416`](https://github.com/eyaltoledano/claude-task-master/commit/18aa416035f44345bde1c7321490345733a5d042), [`18aa416`](https://github.com/eyaltoledano/claude-task-master/commit/18aa416035f44345bde1c7321490345733a5d042), [`738ec51`](https://github.com/eyaltoledano/claude-task-master/commit/738ec51c049a295a12839b2dfddaf05e23b8fede), [`d67b81d`](https://github.com/eyaltoledano/claude-task-master/commit/d67b81d25ddd927fabb6f5deb368e8993519c541), [`b5fe723`](https://github.com/eyaltoledano/claude-task-master/commit/b5fe723f8ead928e9f2dbde13b833ee70ac3382d), [`2b69936`](https://github.com/eyaltoledano/claude-task-master/commit/2b69936ee7b34346d6de5175af20e077359e2e2a), [`986ac11`](https://github.com/eyaltoledano/claude-task-master/commit/986ac117aee00bcd3e6830a0f76e1ad6d10e0bca), [`20004a3`](https://github.com/eyaltoledano/claude-task-master/commit/20004a39ea848f747e1ff48981bfe176554e4055)]:
|
||||||
|
- task-master-ai@0.28.0-rc.0
|
||||||
|
|
||||||
## 0.25.4
|
## 0.25.4
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"displayName": "TaskMaster",
|
"displayName": "TaskMaster",
|
||||||
"description": "A visual Kanban board interface for TaskMaster projects in VS Code",
|
"description": "A visual Kanban board interface for TaskMaster projects in VS Code",
|
||||||
"version": "0.25.4",
|
"version": "0.25.5-rc.0",
|
||||||
"publisher": "Hamster",
|
"publisher": "Hamster",
|
||||||
"icon": "assets/icon.png",
|
"icon": "assets/icon.png",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@@ -383,6 +383,12 @@ task-master models --set-main=my-local-llama --ollama
|
|||||||
# Set a custom OpenRouter model for the research role
|
# Set a custom OpenRouter model for the research role
|
||||||
task-master models --set-research=google/gemini-pro --openrouter
|
task-master models --set-research=google/gemini-pro --openrouter
|
||||||
|
|
||||||
|
# Set Codex CLI model for the main role (uses ChatGPT subscription via OAuth)
|
||||||
|
task-master models --set-main=gpt-5-codex --codex-cli
|
||||||
|
|
||||||
|
# Set Codex CLI model for the fallback role
|
||||||
|
task-master models --set-fallback=gpt-5 --codex-cli
|
||||||
|
|
||||||
# Run interactive setup to configure models, including custom ones
|
# Run interactive setup to configure models, including custom ones
|
||||||
task-master models --setup
|
task-master models --setup
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -429,3 +429,153 @@ Azure OpenAI provides enterprise-grade OpenAI models through Microsoft's Azure c
|
|||||||
- Verify the deployment name matches your configuration exactly (case-sensitive)
|
- Verify the deployment name matches your configuration exactly (case-sensitive)
|
||||||
- Ensure the model deployment is in a "Succeeded" state in Azure OpenAI Studio
|
- Ensure the model deployment is in a "Succeeded" state in Azure OpenAI Studio
|
||||||
- Ensure youre not getting rate limited by `maxTokens` maintain appropriate Tokens per Minute Rate Limit (TPM) in your deployment.
|
- Ensure youre not getting rate limited by `maxTokens` maintain appropriate Tokens per Minute Rate Limit (TPM) in your deployment.
|
||||||
|
|
||||||
|
### Codex CLI Provider
|
||||||
|
|
||||||
|
The Codex CLI provider integrates Task Master with OpenAI's Codex CLI, allowing you to use ChatGPT subscription models via OAuth authentication.
|
||||||
|
|
||||||
|
1. **Prerequisites**:
|
||||||
|
- Node.js >= 18
|
||||||
|
- Codex CLI >= 0.42.0 (>= 0.44.0 recommended)
|
||||||
|
- ChatGPT subscription: Plus, Pro, Business, Edu, or Enterprise (for OAuth access to GPT-5 models)
|
||||||
|
|
||||||
|
2. **Installation**:
|
||||||
|
```bash
|
||||||
|
npm install -g @openai/codex
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Authentication** (OAuth - Primary Method):
|
||||||
|
```bash
|
||||||
|
codex login
|
||||||
|
```
|
||||||
|
This will open a browser window for OAuth authentication with your ChatGPT account. Once authenticated, Task Master will automatically use these credentials.
|
||||||
|
|
||||||
|
4. **Optional API Key Method**:
|
||||||
|
While OAuth is the primary and recommended authentication method, you can optionally set an OpenAI API key:
|
||||||
|
```bash
|
||||||
|
# In .env file
|
||||||
|
OPENAI_API_KEY=sk-your-openai-api-key-here
|
||||||
|
```
|
||||||
|
**Note**: The API key will only be injected if explicitly provided. OAuth is always preferred.
|
||||||
|
|
||||||
|
5. **Configuration**:
|
||||||
|
```json
|
||||||
|
// In .taskmaster/config.json
|
||||||
|
{
|
||||||
|
"models": {
|
||||||
|
"main": {
|
||||||
|
"provider": "codex-cli",
|
||||||
|
"modelId": "gpt-5-codex",
|
||||||
|
"maxTokens": 128000,
|
||||||
|
"temperature": 0.2
|
||||||
|
},
|
||||||
|
"fallback": {
|
||||||
|
"provider": "codex-cli",
|
||||||
|
"modelId": "gpt-5",
|
||||||
|
"maxTokens": 128000,
|
||||||
|
"temperature": 0.2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"codexCli": {
|
||||||
|
"allowNpx": true,
|
||||||
|
"skipGitRepoCheck": true,
|
||||||
|
"approvalMode": "on-failure",
|
||||||
|
"sandboxMode": "workspace-write"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Available Models**:
|
||||||
|
- `gpt-5` - Latest GPT-5 model (272K max input, 128K max output)
|
||||||
|
- `gpt-5-codex` - GPT-5 optimized for agentic software engineering (272K max input, 128K max output)
|
||||||
|
|
||||||
|
7. **Codex CLI Settings (`codexCli` section)**:
|
||||||
|
|
||||||
|
The `codexCli` section in your configuration file supports the following options:
|
||||||
|
|
||||||
|
- **`allowNpx`** (boolean, default: `false`): Allow fallback to `npx @openai/codex` if CLI not found on PATH
|
||||||
|
- **`skipGitRepoCheck`** (boolean, default: `false`): Skip git repository safety check (recommended for CI/non-repo usage)
|
||||||
|
- **`approvalMode`** (string): Control command execution approval
|
||||||
|
- `"untrusted"`: Require approval for all commands
|
||||||
|
- `"on-failure"`: Only require approval after a command fails (default)
|
||||||
|
- `"on-request"`: Approve only when explicitly requested
|
||||||
|
- `"never"`: Never require approval (not recommended)
|
||||||
|
- **`sandboxMode`** (string): Control filesystem access
|
||||||
|
- `"read-only"`: Read-only access
|
||||||
|
- `"workspace-write"`: Allow writes to workspace (default)
|
||||||
|
- `"danger-full-access"`: Full filesystem access (use with caution)
|
||||||
|
- **`codexPath`** (string, optional): Custom path to codex CLI executable
|
||||||
|
- **`cwd`** (string, optional): Working directory for Codex CLI execution
|
||||||
|
- **`fullAuto`** (boolean, optional): Fully automatic mode (equivalent to `--full-auto` flag)
|
||||||
|
- **`dangerouslyBypassApprovalsAndSandbox`** (boolean, optional): Bypass all safety checks (dangerous!)
|
||||||
|
- **`color`** (string, optional): Color handling - `"always"`, `"never"`, or `"auto"`
|
||||||
|
- **`outputLastMessageFile`** (string, optional): Write last agent message to specified file
|
||||||
|
- **`verbose`** (boolean, optional): Enable verbose logging
|
||||||
|
- **`env`** (object, optional): Additional environment variables for Codex CLI
|
||||||
|
|
||||||
|
8. **Command-Specific Settings** (optional):
|
||||||
|
You can override settings for specific Task Master commands:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"codexCli": {
|
||||||
|
"allowNpx": true,
|
||||||
|
"approvalMode": "on-failure",
|
||||||
|
"commandSpecific": {
|
||||||
|
"parse-prd": {
|
||||||
|
"approvalMode": "never",
|
||||||
|
"verbose": true
|
||||||
|
},
|
||||||
|
"expand": {
|
||||||
|
"sandboxMode": "read-only"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
9. **Codebase Features**:
|
||||||
|
The Codex CLI provider is codebase-capable, meaning it can analyze and interact with your project files. Codebase analysis features are automatically enabled when using `codex-cli` as your provider and `enableCodebaseAnalysis` is set to `true` in your global configuration (default).
|
||||||
|
|
||||||
|
10. **Setup Commands**:
|
||||||
|
```bash
|
||||||
|
# Set Codex CLI for main role
|
||||||
|
task-master models --set-main gpt-5-codex --codex-cli
|
||||||
|
|
||||||
|
# Set Codex CLI for fallback role
|
||||||
|
task-master models --set-fallback gpt-5 --codex-cli
|
||||||
|
|
||||||
|
# Verify configuration
|
||||||
|
task-master models
|
||||||
|
```
|
||||||
|
|
||||||
|
11. **Troubleshooting**:
|
||||||
|
|
||||||
|
**"codex: command not found" error:**
|
||||||
|
- Install Codex CLI globally: `npm install -g @openai/codex`
|
||||||
|
- Verify installation: `codex --version`
|
||||||
|
- Alternatively, enable `allowNpx: true` in your codexCli configuration
|
||||||
|
|
||||||
|
**"Not logged in" errors:**
|
||||||
|
- Run `codex login` to authenticate with your ChatGPT account
|
||||||
|
- Verify authentication status: `codex` (opens interactive CLI)
|
||||||
|
|
||||||
|
**"Old version" warnings:**
|
||||||
|
- Check version: `codex --version`
|
||||||
|
- Upgrade: `npm install -g @openai/codex@latest`
|
||||||
|
- Minimum version: 0.42.0, recommended: >= 0.44.0
|
||||||
|
|
||||||
|
**"Model not available" errors:**
|
||||||
|
- Only `gpt-5` and `gpt-5-codex` are available via OAuth subscription
|
||||||
|
- Verify your ChatGPT subscription is active
|
||||||
|
- For other OpenAI models, use the standard `openai` provider with an API key
|
||||||
|
|
||||||
|
**API key not being used:**
|
||||||
|
- API key is only injected when explicitly provided
|
||||||
|
- OAuth authentication is always preferred
|
||||||
|
- If you want to use an API key, ensure `OPENAI_API_KEY` is set in your `.env` file
|
||||||
|
|
||||||
|
12. **Important Notes**:
|
||||||
|
- OAuth subscription required for model access (no API key needed for basic operation)
|
||||||
|
- Limited to OAuth-available models only (`gpt-5` and `gpt-5-codex`)
|
||||||
|
- Pricing information is not available for OAuth models (shows as "Unknown" in cost calculations)
|
||||||
|
- See [Codex CLI Provider Documentation](./providers/codex-cli.md) for more details
|
||||||
|
|||||||
463
docs/examples/codex-cli-usage.md
Normal file
463
docs/examples/codex-cli-usage.md
Normal file
@@ -0,0 +1,463 @@
|
|||||||
|
# Codex CLI Provider Usage Examples
|
||||||
|
|
||||||
|
This guide provides practical examples of using Task Master with the Codex CLI provider.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Before using these examples, ensure you have:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Codex CLI installed
|
||||||
|
npm install -g @openai/codex
|
||||||
|
|
||||||
|
# 2. Authenticated with ChatGPT
|
||||||
|
codex login
|
||||||
|
|
||||||
|
# 3. Codex CLI configured as your provider
|
||||||
|
task-master models --set-main gpt-5-codex --codex-cli
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example 1: Basic Task Creation
|
||||||
|
|
||||||
|
Use Codex CLI to create tasks from a simple description:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Add a task with AI-powered enhancement
|
||||||
|
task-master add-task --prompt="Implement user authentication with JWT" --research
|
||||||
|
```
|
||||||
|
|
||||||
|
**What happens**:
|
||||||
|
1. Task Master sends your prompt to GPT-5-Codex via the CLI
|
||||||
|
2. The AI analyzes your request and generates a detailed task
|
||||||
|
3. The task is added to your `.taskmaster/tasks/tasks.json`
|
||||||
|
4. OAuth credentials are automatically used (no API key needed)
|
||||||
|
|
||||||
|
## Example 2: Parsing a Product Requirements Document
|
||||||
|
|
||||||
|
Create a comprehensive task list from a PRD:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create your PRD
|
||||||
|
cat > my-feature.txt <<EOF
|
||||||
|
# User Profile Feature
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
1. Users can view their profile
|
||||||
|
2. Users can edit their information
|
||||||
|
3. Profile pictures can be uploaded
|
||||||
|
4. Email verification required
|
||||||
|
|
||||||
|
## Technical Constraints
|
||||||
|
- Use React for frontend
|
||||||
|
- Node.js/Express backend
|
||||||
|
- PostgreSQL database
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Parse with Codex CLI
|
||||||
|
task-master parse-prd my-feature.txt --num-tasks 12
|
||||||
|
```
|
||||||
|
|
||||||
|
**What happens**:
|
||||||
|
1. GPT-5-Codex reads and analyzes your PRD
|
||||||
|
2. Generates structured tasks with dependencies
|
||||||
|
3. Creates subtasks for complex items
|
||||||
|
4. Saves everything to `.taskmaster/tasks/`
|
||||||
|
|
||||||
|
## Example 3: Expanding Tasks with Research
|
||||||
|
|
||||||
|
Break down a complex task into detailed subtasks:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# First, show your current tasks
|
||||||
|
task-master list
|
||||||
|
|
||||||
|
# Expand a specific task (e.g., task 1.2)
|
||||||
|
task-master expand --id=1.2 --research --force
|
||||||
|
```
|
||||||
|
|
||||||
|
**What happens**:
|
||||||
|
1. Codex CLI uses GPT-5 for research-level analysis
|
||||||
|
2. Breaks down the task into logical subtasks
|
||||||
|
3. Adds implementation details and test strategies
|
||||||
|
4. Updates the task with dependency information
|
||||||
|
|
||||||
|
## Example 4: Analyzing Project Complexity
|
||||||
|
|
||||||
|
Get AI-powered insights into your project's task complexity:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Analyze all tasks
|
||||||
|
task-master analyze-complexity --research
|
||||||
|
|
||||||
|
# View the complexity report
|
||||||
|
task-master complexity-report
|
||||||
|
```
|
||||||
|
|
||||||
|
**What happens**:
|
||||||
|
1. GPT-5 analyzes each task's scope and requirements
|
||||||
|
2. Assigns complexity scores and estimates subtask counts
|
||||||
|
3. Generates a detailed report
|
||||||
|
4. Saves to `.taskmaster/reports/task-complexity-report.json`
|
||||||
|
|
||||||
|
## Example 5: Using Custom Codex CLI Settings
|
||||||
|
|
||||||
|
Configure Codex CLI behavior for different commands:
|
||||||
|
|
||||||
|
```json
|
||||||
|
// In .taskmaster/config.json
|
||||||
|
{
|
||||||
|
"models": {
|
||||||
|
"main": {
|
||||||
|
"provider": "codex-cli",
|
||||||
|
"modelId": "gpt-5-codex",
|
||||||
|
"maxTokens": 128000,
|
||||||
|
"temperature": 0.2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"codexCli": {
|
||||||
|
"allowNpx": true,
|
||||||
|
"approvalMode": "on-failure",
|
||||||
|
"sandboxMode": "workspace-write",
|
||||||
|
"commandSpecific": {
|
||||||
|
"parse-prd": {
|
||||||
|
"verbose": true,
|
||||||
|
"approvalMode": "never"
|
||||||
|
},
|
||||||
|
"expand": {
|
||||||
|
"sandboxMode": "read-only",
|
||||||
|
"verbose": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Now parse-prd runs with verbose output and no approvals
|
||||||
|
task-master parse-prd requirements.txt
|
||||||
|
|
||||||
|
# Expand runs with read-only mode
|
||||||
|
task-master expand --id=2.1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example 6: Workflow - Building a Feature End-to-End
|
||||||
|
|
||||||
|
Complete workflow from PRD to implementation tracking:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Step 1: Initialize project
|
||||||
|
task-master init
|
||||||
|
|
||||||
|
# Step 2: Set up Codex CLI
|
||||||
|
task-master models --set-main gpt-5-codex --codex-cli
|
||||||
|
task-master models --set-fallback gpt-5 --codex-cli
|
||||||
|
|
||||||
|
# Step 3: Create PRD
|
||||||
|
cat > feature-prd.txt <<EOF
|
||||||
|
# Authentication System
|
||||||
|
|
||||||
|
Implement a complete authentication system with:
|
||||||
|
- User registration
|
||||||
|
- Email verification
|
||||||
|
- Password reset
|
||||||
|
- Two-factor authentication
|
||||||
|
- Session management
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Step 4: Parse PRD into tasks
|
||||||
|
task-master parse-prd feature-prd.txt --num-tasks 8
|
||||||
|
|
||||||
|
# Step 5: Analyze complexity
|
||||||
|
task-master analyze-complexity --research
|
||||||
|
|
||||||
|
# Step 6: Expand complex tasks
|
||||||
|
task-master expand --all --research
|
||||||
|
|
||||||
|
# Step 7: Start working
|
||||||
|
task-master next
|
||||||
|
# Shows: Task 1.1: User registration database schema
|
||||||
|
|
||||||
|
# Step 8: Mark completed as you work
|
||||||
|
task-master set-status --id=1.1 --status=done
|
||||||
|
|
||||||
|
# Step 9: Continue to next task
|
||||||
|
task-master next
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example 7: Multi-Role Configuration
|
||||||
|
|
||||||
|
Use Codex CLI for main tasks, Perplexity for research:
|
||||||
|
|
||||||
|
```json
|
||||||
|
// In .taskmaster/config.json
|
||||||
|
{
|
||||||
|
"models": {
|
||||||
|
"main": {
|
||||||
|
"provider": "codex-cli",
|
||||||
|
"modelId": "gpt-5-codex",
|
||||||
|
"maxTokens": 128000,
|
||||||
|
"temperature": 0.2
|
||||||
|
},
|
||||||
|
"research": {
|
||||||
|
"provider": "perplexity",
|
||||||
|
"modelId": "sonar-pro",
|
||||||
|
"maxTokens": 8700,
|
||||||
|
"temperature": 0.1
|
||||||
|
},
|
||||||
|
"fallback": {
|
||||||
|
"provider": "codex-cli",
|
||||||
|
"modelId": "gpt-5",
|
||||||
|
"maxTokens": 128000,
|
||||||
|
"temperature": 0.2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Main task operations use GPT-5-Codex
|
||||||
|
task-master add-task --prompt="Build REST API endpoint"
|
||||||
|
|
||||||
|
# Research operations use Perplexity
|
||||||
|
task-master analyze-complexity --research
|
||||||
|
|
||||||
|
# Fallback to GPT-5 if needed
|
||||||
|
task-master expand --id=3.2 --force
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example 8: Troubleshooting Common Issues
|
||||||
|
|
||||||
|
### Issue: Codex CLI not found
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check if Codex is installed
|
||||||
|
codex --version
|
||||||
|
|
||||||
|
# If not found, install globally
|
||||||
|
npm install -g @openai/codex
|
||||||
|
|
||||||
|
# Or enable npx fallback in config
|
||||||
|
cat >> .taskmaster/config.json <<EOF
|
||||||
|
{
|
||||||
|
"codexCli": {
|
||||||
|
"allowNpx": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
### Issue: Not authenticated
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check auth status
|
||||||
|
codex
|
||||||
|
# Use /about command to see auth info
|
||||||
|
|
||||||
|
# Re-authenticate if needed
|
||||||
|
codex login
|
||||||
|
```
|
||||||
|
|
||||||
|
### Issue: Want more verbose output
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Enable verbose mode in config
|
||||||
|
cat >> .taskmaster/config.json <<EOF
|
||||||
|
{
|
||||||
|
"codexCli": {
|
||||||
|
"verbose": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Or for specific commands
|
||||||
|
task-master parse-prd my-prd.txt
|
||||||
|
# (verbose output shows detailed Codex CLI interactions)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example 9: CI/CD Integration
|
||||||
|
|
||||||
|
Use Codex CLI in automated workflows:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# .github/workflows/task-analysis.yml
|
||||||
|
name: Analyze Task Complexity
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- '.taskmaster/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- name: Install Task Master
|
||||||
|
run: npm install -g task-master-ai
|
||||||
|
|
||||||
|
- name: Configure Codex CLI
|
||||||
|
run: |
|
||||||
|
npm install -g @openai/codex
|
||||||
|
echo "${{ secrets.OPENAI_CODEX_API_KEY }}" > ~/.codex-auth
|
||||||
|
env:
|
||||||
|
OPENAI_CODEX_API_KEY: ${{ secrets.OPENAI_CODEX_API_KEY }}
|
||||||
|
|
||||||
|
- name: Configure Task Master
|
||||||
|
run: |
|
||||||
|
cat > .taskmaster/config.json <<EOF
|
||||||
|
{
|
||||||
|
"models": {
|
||||||
|
"main": {
|
||||||
|
"provider": "codex-cli",
|
||||||
|
"modelId": "gpt-5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"codexCli": {
|
||||||
|
"allowNpx": true,
|
||||||
|
"skipGitRepoCheck": true,
|
||||||
|
"approvalMode": "never",
|
||||||
|
"fullAuto": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
- name: Analyze Complexity
|
||||||
|
run: task-master analyze-complexity --research
|
||||||
|
|
||||||
|
- name: Upload Report
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: complexity-report
|
||||||
|
path: .taskmaster/reports/task-complexity-report.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### 1. Use OAuth for Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# For local development, use OAuth (no API key needed)
|
||||||
|
codex login
|
||||||
|
task-master models --set-main gpt-5-codex --codex-cli
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Configure Approval Modes Appropriately
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"codexCli": {
|
||||||
|
"approvalMode": "on-failure", // Safe default
|
||||||
|
"sandboxMode": "workspace-write" // Restricts to project directory
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Use Command-Specific Settings
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"codexCli": {
|
||||||
|
"commandSpecific": {
|
||||||
|
"parse-prd": {
|
||||||
|
"approvalMode": "never", // PRD parsing is safe
|
||||||
|
"verbose": true
|
||||||
|
},
|
||||||
|
"expand": {
|
||||||
|
"approvalMode": "on-request", // More cautious for task expansion
|
||||||
|
"verbose": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Leverage Codebase Analysis
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"global": {
|
||||||
|
"enableCodebaseAnalysis": true // Let Codex analyze your code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Handle Errors Gracefully
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Always configure a fallback model
|
||||||
|
task-master models --set-fallback gpt-5 --codex-cli
|
||||||
|
|
||||||
|
# Or use a different provider as fallback
|
||||||
|
task-master models --set-fallback claude-3-5-sonnet
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- Read the [Codex CLI Provider Documentation](../providers/codex-cli.md)
|
||||||
|
- Explore [Configuration Options](../configuration.md#codex-cli-provider)
|
||||||
|
- Check out [Command Reference](../command-reference.md)
|
||||||
|
- Learn about [Task Structure](../task-structure.md)
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Pattern: Daily Development Workflow
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Morning: Review tasks
|
||||||
|
task-master list
|
||||||
|
|
||||||
|
# Get next task
|
||||||
|
task-master next
|
||||||
|
|
||||||
|
# Work on task...
|
||||||
|
|
||||||
|
# Update task with notes
|
||||||
|
task-master update-subtask --id=2.3 --prompt="Implemented authentication middleware"
|
||||||
|
|
||||||
|
# Mark complete
|
||||||
|
task-master set-status --id=2.3 --status=done
|
||||||
|
|
||||||
|
# Repeat
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern: Feature Planning
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Write feature spec
|
||||||
|
vim new-feature.txt
|
||||||
|
|
||||||
|
# Generate tasks
|
||||||
|
task-master parse-prd new-feature.txt --num-tasks 10
|
||||||
|
|
||||||
|
# Analyze and expand
|
||||||
|
task-master analyze-complexity --research
|
||||||
|
task-master expand --all --research --force
|
||||||
|
|
||||||
|
# Review and adjust
|
||||||
|
task-master list
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern: Sprint Planning
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Parse sprint requirements
|
||||||
|
task-master parse-prd sprint-requirements.txt
|
||||||
|
|
||||||
|
# Analyze complexity
|
||||||
|
task-master analyze-complexity --research
|
||||||
|
|
||||||
|
# View report
|
||||||
|
task-master complexity-report
|
||||||
|
|
||||||
|
# Adjust task estimates based on complexity scores
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
For more examples and advanced usage, see the [full documentation](https://docs.task-master.dev).
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# Available Models as of September 23, 2025
|
# Available Models as of October 5, 2025
|
||||||
|
|
||||||
## Main Models
|
## Main Models
|
||||||
|
|
||||||
@@ -10,6 +10,8 @@
|
|||||||
| anthropic | claude-3-5-sonnet-20241022 | 0.49 | 3 | 15 |
|
| anthropic | claude-3-5-sonnet-20241022 | 0.49 | 3 | 15 |
|
||||||
| claude-code | opus | 0.725 | 0 | 0 |
|
| claude-code | opus | 0.725 | 0 | 0 |
|
||||||
| claude-code | sonnet | 0.727 | 0 | 0 |
|
| claude-code | sonnet | 0.727 | 0 | 0 |
|
||||||
|
| codex-cli | gpt-5 | 0.749 | 0 | 0 |
|
||||||
|
| codex-cli | gpt-5-codex | 0.749 | 0 | 0 |
|
||||||
| mcp | mcp-sampling | — | 0 | 0 |
|
| mcp | mcp-sampling | — | 0 | 0 |
|
||||||
| gemini-cli | gemini-2.5-pro | 0.72 | 0 | 0 |
|
| gemini-cli | gemini-2.5-pro | 0.72 | 0 | 0 |
|
||||||
| gemini-cli | gemini-2.5-flash | 0.71 | 0 | 0 |
|
| gemini-cli | gemini-2.5-flash | 0.71 | 0 | 0 |
|
||||||
@@ -100,6 +102,8 @@
|
|||||||
| ----------- | -------------------------------------------- | --------- | ---------- | ----------- |
|
| ----------- | -------------------------------------------- | --------- | ---------- | ----------- |
|
||||||
| claude-code | opus | 0.725 | 0 | 0 |
|
| claude-code | opus | 0.725 | 0 | 0 |
|
||||||
| claude-code | sonnet | 0.727 | 0 | 0 |
|
| claude-code | sonnet | 0.727 | 0 | 0 |
|
||||||
|
| codex-cli | gpt-5 | 0.749 | 0 | 0 |
|
||||||
|
| codex-cli | gpt-5-codex | 0.749 | 0 | 0 |
|
||||||
| mcp | mcp-sampling | — | 0 | 0 |
|
| mcp | mcp-sampling | — | 0 | 0 |
|
||||||
| gemini-cli | gemini-2.5-pro | 0.72 | 0 | 0 |
|
| gemini-cli | gemini-2.5-pro | 0.72 | 0 | 0 |
|
||||||
| gemini-cli | gemini-2.5-flash | 0.71 | 0 | 0 |
|
| gemini-cli | gemini-2.5-flash | 0.71 | 0 | 0 |
|
||||||
@@ -140,6 +144,8 @@
|
|||||||
| anthropic | claude-3-5-sonnet-20241022 | 0.49 | 3 | 15 |
|
| anthropic | claude-3-5-sonnet-20241022 | 0.49 | 3 | 15 |
|
||||||
| claude-code | opus | 0.725 | 0 | 0 |
|
| claude-code | opus | 0.725 | 0 | 0 |
|
||||||
| claude-code | sonnet | 0.727 | 0 | 0 |
|
| claude-code | sonnet | 0.727 | 0 | 0 |
|
||||||
|
| codex-cli | gpt-5 | 0.749 | 0 | 0 |
|
||||||
|
| codex-cli | gpt-5-codex | 0.749 | 0 | 0 |
|
||||||
| mcp | mcp-sampling | — | 0 | 0 |
|
| mcp | mcp-sampling | — | 0 | 0 |
|
||||||
| gemini-cli | gemini-2.5-pro | 0.72 | 0 | 0 |
|
| gemini-cli | gemini-2.5-pro | 0.72 | 0 | 0 |
|
||||||
| gemini-cli | gemini-2.5-flash | 0.71 | 0 | 0 |
|
| gemini-cli | gemini-2.5-flash | 0.71 | 0 | 0 |
|
||||||
|
|||||||
510
docs/providers/codex-cli.md
Normal file
510
docs/providers/codex-cli.md
Normal file
@@ -0,0 +1,510 @@
|
|||||||
|
# Codex CLI Provider
|
||||||
|
|
||||||
|
The `codex-cli` provider integrates Task Master with OpenAI's Codex CLI via the community AI SDK provider [`ai-sdk-provider-codex-cli`](https://github.com/ben-vargas/ai-sdk-provider-codex-cli). It uses your ChatGPT subscription (OAuth) via `codex login`, with optional `OPENAI_CODEX_API_KEY` support.
|
||||||
|
|
||||||
|
## Why Use Codex CLI?
|
||||||
|
|
||||||
|
The primary benefits of using the `codex-cli` provider include:
|
||||||
|
|
||||||
|
- **Use Latest OpenAI Models**: Access to cutting-edge models like GPT-5 and GPT-5-Codex via ChatGPT subscription
|
||||||
|
- **OAuth Authentication**: No API key management needed - authenticate once with `codex login`
|
||||||
|
- **Built-in Tool Execution**: Native support for command execution, file changes, MCP tools, and web search
|
||||||
|
- **Native JSON Schema Support**: Structured output generation without post-processing
|
||||||
|
- **Approval/Sandbox Modes**: Fine-grained control over command execution and filesystem access for safety
|
||||||
|
|
||||||
|
## Quickstart
|
||||||
|
|
||||||
|
Get up and running with Codex CLI in 3 steps:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Install Codex CLI globally
|
||||||
|
npm install -g @openai/codex
|
||||||
|
|
||||||
|
# 2. Authenticate with your ChatGPT account
|
||||||
|
codex login
|
||||||
|
|
||||||
|
# 3. Configure Task Master to use Codex CLI
|
||||||
|
task-master models --set-main gpt-5-codex --codex-cli
|
||||||
|
```
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- **Node.js**: >= 18.0.0
|
||||||
|
- **Codex CLI**: >= 0.42.0 (>= 0.44.0 recommended)
|
||||||
|
- **ChatGPT Subscription**: Required for OAuth access (Plus, Pro, Business, Edu, or Enterprise)
|
||||||
|
- **Task Master**: >= 0.27.3 (version with Codex CLI support)
|
||||||
|
|
||||||
|
### Checking Your Versions
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check Node.js version
|
||||||
|
node --version
|
||||||
|
|
||||||
|
# Check Codex CLI version
|
||||||
|
codex --version
|
||||||
|
|
||||||
|
# Check Task Master version
|
||||||
|
task-master --version
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Install Codex CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install globally via npm
|
||||||
|
npm install -g @openai/codex
|
||||||
|
|
||||||
|
# Verify installation
|
||||||
|
codex --version
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected output: `v0.44.0` or higher
|
||||||
|
|
||||||
|
### Install Task Master (if not already installed)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install globally
|
||||||
|
npm install -g task-master-ai
|
||||||
|
|
||||||
|
# Or install in your project
|
||||||
|
npm install --save-dev task-master-ai
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
### OAuth Authentication (Primary Method - Recommended)
|
||||||
|
|
||||||
|
The Codex CLI provider is designed to use OAuth authentication with your ChatGPT subscription:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Launch Codex CLI and authenticate
|
||||||
|
codex login
|
||||||
|
```
|
||||||
|
|
||||||
|
This will:
|
||||||
|
1. Open a browser window for OAuth authentication
|
||||||
|
2. Prompt you to log in with your ChatGPT account
|
||||||
|
3. Store authentication credentials locally
|
||||||
|
4. Allow Task Master to automatically use these credentials
|
||||||
|
|
||||||
|
To verify your authentication:
|
||||||
|
```bash
|
||||||
|
# Open interactive Codex CLI
|
||||||
|
codex
|
||||||
|
|
||||||
|
# Use /about command to see auth status
|
||||||
|
/about
|
||||||
|
```
|
||||||
|
|
||||||
|
### Optional: API Key Method
|
||||||
|
|
||||||
|
While OAuth is the primary and recommended method, you can optionally use an OpenAI API key:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# In your .env file
|
||||||
|
OPENAI_CODEX_API_KEY=sk-your-openai-api-key-here
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important Notes**:
|
||||||
|
- The API key will **only** be injected when explicitly provided
|
||||||
|
- OAuth authentication is always preferred when available
|
||||||
|
- Using an API key doesn't provide access to subscription-only models like GPT-5-Codex
|
||||||
|
- For full OpenAI API access with non-subscription models, consider using the standard `openai` provider instead
|
||||||
|
- `OPENAI_CODEX_API_KEY` is specific to the codex-cli provider to avoid conflicts with the `openai` provider's `OPENAI_API_KEY`
|
||||||
|
|
||||||
|
## Available Models
|
||||||
|
|
||||||
|
The Codex CLI provider supports only models available through ChatGPT subscription:
|
||||||
|
|
||||||
|
| Model ID | Description | Max Input Tokens | Max Output Tokens |
|
||||||
|
|----------|-------------|------------------|-------------------|
|
||||||
|
| `gpt-5` | Latest GPT-5 model | 272K | 128K |
|
||||||
|
| `gpt-5-codex` | GPT-5 optimized for agentic software engineering | 272K | 128K |
|
||||||
|
|
||||||
|
**Note**: These models are only available via OAuth subscription through Codex CLI (ChatGPT Plus, Pro, Business, Edu, or Enterprise plans). For other OpenAI models, use the standard `openai` provider with an API key.
|
||||||
|
|
||||||
|
**Research Capabilities**: Both GPT-5 models support web search tools, making them suitable for the `research` role in addition to `main` and `fallback` roles.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Basic Configuration
|
||||||
|
|
||||||
|
Add Codex CLI to your `.taskmaster/config.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"models": {
|
||||||
|
"main": {
|
||||||
|
"provider": "codex-cli",
|
||||||
|
"modelId": "gpt-5-codex",
|
||||||
|
"maxTokens": 128000,
|
||||||
|
"temperature": 0.2
|
||||||
|
},
|
||||||
|
"fallback": {
|
||||||
|
"provider": "codex-cli",
|
||||||
|
"modelId": "gpt-5",
|
||||||
|
"maxTokens": 128000,
|
||||||
|
"temperature": 0.2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Advanced Configuration with Codex CLI Settings
|
||||||
|
|
||||||
|
The `codexCli` section allows you to customize Codex CLI behavior:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"models": {
|
||||||
|
"main": {
|
||||||
|
"provider": "codex-cli",
|
||||||
|
"modelId": "gpt-5-codex",
|
||||||
|
"maxTokens": 128000,
|
||||||
|
"temperature": 0.2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"codexCli": {
|
||||||
|
"allowNpx": true,
|
||||||
|
"skipGitRepoCheck": true,
|
||||||
|
"approvalMode": "on-failure",
|
||||||
|
"sandboxMode": "workspace-write",
|
||||||
|
"verbose": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Codex CLI Settings Reference
|
||||||
|
|
||||||
|
#### Core Settings
|
||||||
|
|
||||||
|
- **`allowNpx`** (boolean, default: `false`)
|
||||||
|
- Allow fallback to `npx @openai/codex` if the CLI is not found on PATH
|
||||||
|
- Useful for CI environments or systems without global npm installations
|
||||||
|
- Example: `"allowNpx": true`
|
||||||
|
|
||||||
|
- **`skipGitRepoCheck`** (boolean, default: `false`)
|
||||||
|
- Skip git repository safety check before execution
|
||||||
|
- Recommended for CI environments or non-repository usage
|
||||||
|
- Example: `"skipGitRepoCheck": true`
|
||||||
|
|
||||||
|
#### Execution Control
|
||||||
|
|
||||||
|
- **`approvalMode`** (string)
|
||||||
|
- Controls when to require user approval for command execution
|
||||||
|
- Options:
|
||||||
|
- `"untrusted"`: Require approval for all commands
|
||||||
|
- `"on-failure"`: Only require approval after a command fails (default)
|
||||||
|
- `"on-request"`: Approve only when explicitly requested
|
||||||
|
- `"never"`: Never require approval (use with caution)
|
||||||
|
- Example: `"approvalMode": "on-failure"`
|
||||||
|
|
||||||
|
- **`sandboxMode`** (string)
|
||||||
|
- Controls filesystem access permissions
|
||||||
|
- Options:
|
||||||
|
- `"read-only"`: Read-only access to filesystem
|
||||||
|
- `"workspace-write"`: Allow writes to workspace directory (default)
|
||||||
|
- `"danger-full-access"`: Full filesystem access (use with extreme caution)
|
||||||
|
- Example: `"sandboxMode": "workspace-write"`
|
||||||
|
|
||||||
|
#### Path and Environment
|
||||||
|
|
||||||
|
- **`codexPath`** (string, optional)
|
||||||
|
- Custom path to Codex CLI executable
|
||||||
|
- Useful when Codex is installed in a non-standard location
|
||||||
|
- Example: `"codexPath": "/usr/local/bin/codex"`
|
||||||
|
|
||||||
|
- **`cwd`** (string, optional)
|
||||||
|
- Working directory for Codex CLI execution
|
||||||
|
- Defaults to current working directory
|
||||||
|
- Example: `"cwd": "/path/to/project"`
|
||||||
|
|
||||||
|
- **`env`** (object, optional)
|
||||||
|
- Additional environment variables for Codex CLI
|
||||||
|
- Example: `"env": { "DEBUG": "true" }`
|
||||||
|
|
||||||
|
#### Advanced Settings
|
||||||
|
|
||||||
|
- **`fullAuto`** (boolean, optional)
|
||||||
|
- Fully automatic mode (equivalent to `--full-auto` flag)
|
||||||
|
- Bypasses most approvals for fully automated workflows
|
||||||
|
- Example: `"fullAuto": true`
|
||||||
|
|
||||||
|
- **`dangerouslyBypassApprovalsAndSandbox`** (boolean, optional)
|
||||||
|
- Bypass all safety checks including approvals and sandbox
|
||||||
|
- **WARNING**: Use with extreme caution - can execute arbitrary code
|
||||||
|
- Example: `"dangerouslyBypassApprovalsAndSandbox": false`
|
||||||
|
|
||||||
|
- **`color`** (string, optional)
|
||||||
|
- Force color handling in Codex CLI output
|
||||||
|
- Options: `"always"`, `"never"`, `"auto"`
|
||||||
|
- Example: `"color": "auto"`
|
||||||
|
|
||||||
|
- **`outputLastMessageFile`** (string, optional)
|
||||||
|
- Write last agent message to specified file
|
||||||
|
- Useful for debugging or logging
|
||||||
|
- Example: `"outputLastMessageFile": "./last-message.txt"`
|
||||||
|
|
||||||
|
- **`verbose`** (boolean, optional)
|
||||||
|
- Enable verbose provider logging
|
||||||
|
- Helpful for debugging issues
|
||||||
|
- Example: `"verbose": true`
|
||||||
|
|
||||||
|
### Command-Specific Settings
|
||||||
|
|
||||||
|
Override settings for specific Task Master commands:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"codexCli": {
|
||||||
|
"allowNpx": true,
|
||||||
|
"approvalMode": "on-failure",
|
||||||
|
"commandSpecific": {
|
||||||
|
"parse-prd": {
|
||||||
|
"approvalMode": "never",
|
||||||
|
"verbose": true
|
||||||
|
},
|
||||||
|
"expand": {
|
||||||
|
"sandboxMode": "read-only"
|
||||||
|
},
|
||||||
|
"add-task": {
|
||||||
|
"approvalMode": "untrusted"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Setting Codex CLI Models
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Set Codex CLI for main role
|
||||||
|
task-master models --set-main gpt-5-codex --codex-cli
|
||||||
|
|
||||||
|
# Set Codex CLI for fallback role
|
||||||
|
task-master models --set-fallback gpt-5 --codex-cli
|
||||||
|
|
||||||
|
# Set Codex CLI for research role
|
||||||
|
task-master models --set-research gpt-5 --codex-cli
|
||||||
|
|
||||||
|
# Verify configuration
|
||||||
|
task-master models
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Codex CLI with Task Master Commands
|
||||||
|
|
||||||
|
Once configured, use Task Master commands as normal:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Parse a PRD with Codex CLI
|
||||||
|
task-master parse-prd my-requirements.txt
|
||||||
|
|
||||||
|
# Analyze project complexity
|
||||||
|
task-master analyze-complexity --research
|
||||||
|
|
||||||
|
# Expand a task into subtasks
|
||||||
|
task-master expand --id=1.2
|
||||||
|
|
||||||
|
# Add a new task with AI assistance
|
||||||
|
task-master add-task --prompt="Implement user authentication" --research
|
||||||
|
```
|
||||||
|
|
||||||
|
The provider will automatically use your OAuth credentials when Codex CLI is configured.
|
||||||
|
|
||||||
|
## Codebase Features
|
||||||
|
|
||||||
|
The Codex CLI provider is **codebase-capable**, meaning it can analyze and interact with your project files. This enables advanced features like:
|
||||||
|
|
||||||
|
- **Code Analysis**: Understanding your project structure and dependencies
|
||||||
|
- **Intelligent Suggestions**: Context-aware task recommendations
|
||||||
|
- **File Operations**: Reading and analyzing project files for better task generation
|
||||||
|
- **Pattern Recognition**: Identifying common patterns and best practices in your codebase
|
||||||
|
|
||||||
|
### Enabling Codebase Analysis
|
||||||
|
|
||||||
|
Codebase analysis is automatically enabled when:
|
||||||
|
1. Your provider is set to `codex-cli`
|
||||||
|
2. `enableCodebaseAnalysis` is `true` in your global configuration (default)
|
||||||
|
|
||||||
|
To verify or configure:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"global": {
|
||||||
|
"enableCodebaseAnalysis": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "codex: command not found" Error
|
||||||
|
|
||||||
|
**Symptoms**: Task Master reports that the Codex CLI is not found.
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
1. **Install Codex CLI globally**:
|
||||||
|
```bash
|
||||||
|
npm install -g @openai/codex
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Verify installation**:
|
||||||
|
```bash
|
||||||
|
codex --version
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Alternative: Enable npx fallback**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"codexCli": {
|
||||||
|
"allowNpx": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Not logged in" Errors
|
||||||
|
|
||||||
|
**Symptoms**: Authentication errors when trying to use Codex CLI.
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
1. **Authenticate with OAuth**:
|
||||||
|
```bash
|
||||||
|
codex login
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Verify authentication status**:
|
||||||
|
```bash
|
||||||
|
codex
|
||||||
|
# Then use /about command
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Re-authenticate if needed**:
|
||||||
|
```bash
|
||||||
|
# Logout first
|
||||||
|
codex
|
||||||
|
# Use /auth command to change auth method
|
||||||
|
|
||||||
|
# Then login again
|
||||||
|
codex login
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Old version" Warnings
|
||||||
|
|
||||||
|
**Symptoms**: Warnings about Codex CLI version being outdated.
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
1. **Check current version**:
|
||||||
|
```bash
|
||||||
|
codex --version
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Upgrade to latest version**:
|
||||||
|
```bash
|
||||||
|
npm install -g @openai/codex@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Verify upgrade**:
|
||||||
|
```bash
|
||||||
|
codex --version
|
||||||
|
```
|
||||||
|
Should show >= 0.44.0
|
||||||
|
|
||||||
|
### "Model not available" Errors
|
||||||
|
|
||||||
|
**Symptoms**: Error indicating the requested model is not available.
|
||||||
|
|
||||||
|
**Causes and Solutions**:
|
||||||
|
|
||||||
|
1. **Using unsupported model**:
|
||||||
|
- Only `gpt-5` and `gpt-5-codex` are available via Codex CLI
|
||||||
|
- For other OpenAI models, use the standard `openai` provider
|
||||||
|
|
||||||
|
2. **Subscription not active**:
|
||||||
|
- Verify your ChatGPT subscription is active
|
||||||
|
- Check subscription status at <https://platform.openai.com>
|
||||||
|
|
||||||
|
3. **Wrong provider selected**:
|
||||||
|
- Verify you're using `--codex-cli` flag when setting models
|
||||||
|
- Check `.taskmaster/config.json` shows `"provider": "codex-cli"`
|
||||||
|
|
||||||
|
### API Key Not Being Used
|
||||||
|
|
||||||
|
**Symptoms**: You've set `OPENAI_CODEX_API_KEY` but it's not being used.
|
||||||
|
|
||||||
|
**Expected Behavior**:
|
||||||
|
- OAuth authentication is always preferred
|
||||||
|
- API key is only injected when explicitly provided
|
||||||
|
- API key doesn't grant access to subscription-only models
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
1. **Verify OAuth is working**:
|
||||||
|
```bash
|
||||||
|
codex
|
||||||
|
# Check /about for auth status
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **If you want to force API key usage**:
|
||||||
|
- This is not recommended with Codex CLI
|
||||||
|
- Consider using the standard `openai` provider instead
|
||||||
|
|
||||||
|
3. **Verify .env file is being loaded**:
|
||||||
|
```bash
|
||||||
|
# Check if .env exists in project root
|
||||||
|
ls -la .env
|
||||||
|
|
||||||
|
# Verify OPENAI_CODEX_API_KEY is set
|
||||||
|
grep OPENAI_CODEX_API_KEY .env
|
||||||
|
```
|
||||||
|
|
||||||
|
### Approval/Sandbox Issues
|
||||||
|
|
||||||
|
**Symptoms**: Commands are blocked or filesystem access is denied.
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
|
||||||
|
1. **Adjust approval mode**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"codexCli": {
|
||||||
|
"approvalMode": "on-request"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Adjust sandbox mode**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"codexCli": {
|
||||||
|
"sandboxMode": "workspace-write"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **For fully automated workflows** (use cautiously):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"codexCli": {
|
||||||
|
"fullAuto": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
- **OAuth subscription required**: No API key needed for basic operation, but requires active ChatGPT subscription
|
||||||
|
- **Limited model selection**: Only `gpt-5` and `gpt-5-codex` available via OAuth
|
||||||
|
- **Pricing information**: Not available for OAuth models (shows as "Unknown" in cost calculations)
|
||||||
|
- **No automatic dependency**: The `@openai/codex` package is not added to Task Master's dependencies - install it globally or enable `allowNpx`
|
||||||
|
- **Codebase analysis**: Automatically enabled when using `codex-cli` provider
|
||||||
|
- **Safety first**: Default settings prioritize safety with `approvalMode: "on-failure"` and `sandboxMode: "workspace-write"`
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [Configuration Guide](../configuration.md#codex-cli-provider) - Complete Codex CLI configuration reference
|
||||||
|
- [Command Reference](../command-reference.md) - Using `--codex-cli` flag with commands
|
||||||
|
- [Gemini CLI Provider](./gemini-cli.md) - Similar CLI-based provider for Google Gemini
|
||||||
|
- [Claude Code Integration](../claude-code-integration.md) - Another CLI-based provider
|
||||||
|
- [ai-sdk-provider-codex-cli](https://github.com/ben-vargas/ai-sdk-provider-codex-cli) - Source code for the provider package
|
||||||
@@ -69,11 +69,29 @@ export function resolveTasksPath(args, log = silentLogger) {
|
|||||||
|
|
||||||
// Use core findTasksPath with explicit path and normalized projectRoot context
|
// Use core findTasksPath with explicit path and normalized projectRoot context
|
||||||
if (projectRoot) {
|
if (projectRoot) {
|
||||||
return coreFindTasksPath(explicitPath, { projectRoot }, log);
|
const foundPath = coreFindTasksPath(explicitPath, { projectRoot }, log);
|
||||||
|
// If core function returns null and no explicit path was provided,
|
||||||
|
// construct the expected default path as documented
|
||||||
|
if (foundPath === null && !explicitPath) {
|
||||||
|
const defaultPath = path.join(
|
||||||
|
projectRoot,
|
||||||
|
'.taskmaster',
|
||||||
|
'tasks',
|
||||||
|
'tasks.json'
|
||||||
|
);
|
||||||
|
log?.info?.(
|
||||||
|
`Core findTasksPath returned null, using default path: ${defaultPath}`
|
||||||
|
);
|
||||||
|
return defaultPath;
|
||||||
|
}
|
||||||
|
return foundPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to core function without projectRoot context
|
// Fallback to core function without projectRoot context
|
||||||
return coreFindTasksPath(explicitPath, null, log);
|
const foundPath = coreFindTasksPath(explicitPath, null, log);
|
||||||
|
// Note: When no projectRoot is available, we can't construct a default path
|
||||||
|
// so we return null and let the calling code handle the error
|
||||||
|
return foundPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
49
output.txt
Normal file
49
output.txt
Normal file
File diff suppressed because one or more lines are too long
139
package-lock.json
generated
139
package-lock.json
generated
@@ -33,6 +33,7 @@
|
|||||||
"@supabase/supabase-js": "^2.57.4",
|
"@supabase/supabase-js": "^2.57.4",
|
||||||
"ai": "^5.0.51",
|
"ai": "^5.0.51",
|
||||||
"ai-sdk-provider-claude-code": "^1.1.4",
|
"ai-sdk-provider-claude-code": "^1.1.4",
|
||||||
|
"ai-sdk-provider-codex-cli": "^0.3.0",
|
||||||
"ai-sdk-provider-gemini-cli": "^1.1.1",
|
"ai-sdk-provider-gemini-cli": "^1.1.1",
|
||||||
"ajv": "^8.17.1",
|
"ajv": "^8.17.1",
|
||||||
"ajv-formats": "^3.0.1",
|
"ajv-formats": "^3.0.1",
|
||||||
@@ -634,6 +635,7 @@
|
|||||||
"apps/extension/node_modules/zod": {
|
"apps/extension/node_modules/zod": {
|
||||||
"version": "3.25.76",
|
"version": "3.25.76",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
@@ -1828,6 +1830,7 @@
|
|||||||
"version": "7.28.4",
|
"version": "7.28.4",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
"@babel/generator": "^7.28.3",
|
"@babel/generator": "^7.28.3",
|
||||||
@@ -2660,6 +2663,7 @@
|
|||||||
"version": "6.3.1",
|
"version": "6.3.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/accessibility": "^3.1.1",
|
"@dnd-kit/accessibility": "^3.1.1",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
@@ -4579,7 +4583,6 @@
|
|||||||
"version": "0.23.2",
|
"version": "0.23.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0"
|
"loose-envify": "^1.1.0"
|
||||||
}
|
}
|
||||||
@@ -5169,7 +5172,6 @@
|
|||||||
"version": "0.23.2",
|
"version": "0.23.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0"
|
"loose-envify": "^1.1.0"
|
||||||
}
|
}
|
||||||
@@ -5178,6 +5180,7 @@
|
|||||||
"version": "3.25.76",
|
"version": "3.25.76",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
@@ -5468,6 +5471,7 @@
|
|||||||
"node_modules/@modelcontextprotocol/sdk/node_modules/zod": {
|
"node_modules/@modelcontextprotocol/sdk/node_modules/zod": {
|
||||||
"version": "3.25.76",
|
"version": "3.25.76",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
@@ -5533,6 +5537,19 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@openai/codex": {
|
||||||
|
"version": "0.44.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.44.0.tgz",
|
||||||
|
"integrity": "sha512-5QNxwcuNn1aZMIzBs9E//vVLLRTZ8jkJRZas2XJgYdBNiSSlGzIuOfPBPXPNiQ2hRPKVqI4/APWIck4jxhw2KA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"bin": {
|
||||||
|
"codex": "bin/codex.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@openapi-contrib/openapi-schema-to-json-schema": {
|
"node_modules/@openapi-contrib/openapi-schema-to-json-schema": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -5555,6 +5572,7 @@
|
|||||||
"node_modules/@opentelemetry/api": {
|
"node_modules/@opentelemetry/api": {
|
||||||
"version": "1.9.0",
|
"version": "1.9.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8.0.0"
|
"node": ">=8.0.0"
|
||||||
}
|
}
|
||||||
@@ -8574,6 +8592,7 @@
|
|||||||
"version": "19.1.8",
|
"version": "19.1.8",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
}
|
}
|
||||||
@@ -8582,6 +8601,7 @@
|
|||||||
"version": "19.1.6",
|
"version": "19.1.6",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.0.0"
|
"@types/react": "^19.0.0"
|
||||||
}
|
}
|
||||||
@@ -9027,6 +9047,7 @@
|
|||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.15.0",
|
"version": "8.15.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -9092,6 +9113,7 @@
|
|||||||
"node_modules/ai": {
|
"node_modules/ai": {
|
||||||
"version": "5.0.57",
|
"version": "5.0.57",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/gateway": "1.0.30",
|
"@ai-sdk/gateway": "1.0.30",
|
||||||
"@ai-sdk/provider": "2.0.0",
|
"@ai-sdk/provider": "2.0.0",
|
||||||
@@ -9162,6 +9184,53 @@
|
|||||||
"@img/sharp-win32-x64": "^0.33.5"
|
"@img/sharp-win32-x64": "^0.33.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ai-sdk-provider-codex-cli": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ai-sdk-provider-codex-cli/-/ai-sdk-provider-codex-cli-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-Qz3fQMC4XqTpvaTOk+Zu9I70lf1mq74komvkc8Vp4hwVOglrqZbGWWCniZ1/4v7m7SFEoG6xK6c8QgsSozLq6g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@ai-sdk/provider": "2.0.0",
|
||||||
|
"@ai-sdk/provider-utils": "3.0.3",
|
||||||
|
"jsonc-parser": "^3.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@openai/codex": "^0.44.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"zod": "^3.0.0 || ^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ai-sdk-provider-codex-cli/node_modules/@ai-sdk/provider-utils": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-kAxIw1nYmFW1g5TvE54ZB3eNtgZna0RnLjPUp1ltz1+t9xkXJIuDT4atrwfau9IbS0BOef38wqrI8CjFfQrxhw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@ai-sdk/provider": "2.0.0",
|
||||||
|
"@standard-schema/spec": "^1.0.0",
|
||||||
|
"eventsource-parser": "^3.0.3",
|
||||||
|
"zod-to-json-schema": "^3.24.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"zod": "^3.25.76 || ^4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ai-sdk-provider-codex-cli/node_modules/@ai-sdk/provider-utils/node_modules/zod-to-json-schema": {
|
||||||
|
"version": "3.24.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz",
|
||||||
|
"integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"peerDependencies": {
|
||||||
|
"zod": "^3.24.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ai-sdk-provider-gemini-cli": {
|
"node_modules/ai-sdk-provider-gemini-cli": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -9264,6 +9333,7 @@
|
|||||||
"node_modules/ajv": {
|
"node_modules/ajv": {
|
||||||
"version": "8.17.1",
|
"version": "8.17.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
"fast-uri": "^3.0.1",
|
"fast-uri": "^3.0.1",
|
||||||
@@ -10269,6 +10339,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.8.3",
|
"baseline-browser-mapping": "^2.8.3",
|
||||||
"caniuse-lite": "^1.0.30001741",
|
"caniuse-lite": "^1.0.30001741",
|
||||||
@@ -12132,7 +12203,8 @@
|
|||||||
"node_modules/devtools-protocol": {
|
"node_modules/devtools-protocol": {
|
||||||
"version": "0.0.1312386",
|
"version": "0.0.1312386",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/dezalgo": {
|
"node_modules/dezalgo": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
@@ -12726,6 +12798,7 @@
|
|||||||
"version": "0.25.10",
|
"version": "0.25.10",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"esbuild": "bin/esbuild"
|
"esbuild": "bin/esbuild"
|
||||||
},
|
},
|
||||||
@@ -13038,6 +13111,7 @@
|
|||||||
"node_modules/express": {
|
"node_modules/express": {
|
||||||
"version": "4.21.2",
|
"version": "4.21.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"accepts": "~1.3.8",
|
"accepts": "~1.3.8",
|
||||||
"array-flatten": "1.1.1",
|
"array-flatten": "1.1.1",
|
||||||
@@ -15391,6 +15465,7 @@
|
|||||||
"version": "6.3.1",
|
"version": "6.3.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alcalzone/ansi-tokenize": "^0.2.0",
|
"@alcalzone/ansi-tokenize": "^0.2.0",
|
||||||
"ansi-escapes": "^7.0.0",
|
"ansi-escapes": "^7.0.0",
|
||||||
@@ -16348,6 +16423,7 @@
|
|||||||
"version": "29.7.0",
|
"version": "29.7.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jest/core": "^29.7.0",
|
"@jest/core": "^29.7.0",
|
||||||
"@jest/types": "^29.6.3",
|
"@jest/types": "^29.6.3",
|
||||||
@@ -17965,6 +18041,7 @@
|
|||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 10.16.0"
|
"node": ">= 10.16.0"
|
||||||
}
|
}
|
||||||
@@ -18290,7 +18367,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
},
|
},
|
||||||
@@ -18515,7 +18591,6 @@
|
|||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||||
},
|
},
|
||||||
@@ -18646,6 +18721,7 @@
|
|||||||
"node_modules/marked": {
|
"node_modules/marked": {
|
||||||
"version": "15.0.12",
|
"version": "15.0.12",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"marked": "bin/marked.js"
|
"marked": "bin/marked.js"
|
||||||
},
|
},
|
||||||
@@ -21368,6 +21444,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
@@ -22750,6 +22827,7 @@
|
|||||||
"integrity": "sha512-U+NPR0Bkg3wm61dteD2L4nAM1U9dtaqVrpDXwC36IKRHpEO/Ubpid4Nijpa2imPchcVNHfxVFwSSMJdwdGFUbg==",
|
"integrity": "sha512-U+NPR0Bkg3wm61dteD2L4nAM1U9dtaqVrpDXwC36IKRHpEO/Ubpid4Nijpa2imPchcVNHfxVFwSSMJdwdGFUbg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oxc-project/types": "=0.93.0",
|
"@oxc-project/types": "=0.93.0",
|
||||||
"@rolldown/pluginutils": "1.0.0-beta.41",
|
"@rolldown/pluginutils": "1.0.0-beta.41",
|
||||||
@@ -24982,18 +25060,6 @@
|
|||||||
"version": "0.3.2",
|
"version": "0.3.2",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/tsup/node_modules/yaml": {
|
|
||||||
"version": "2.8.1",
|
|
||||||
"license": "ISC",
|
|
||||||
"optional": true,
|
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
|
||||||
"yaml": "bin.mjs"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 14.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tsx": {
|
"node_modules/tsx": {
|
||||||
"version": "4.20.6",
|
"version": "4.20.6",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
@@ -25190,6 +25256,7 @@
|
|||||||
"version": "5.9.2",
|
"version": "5.9.2",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -25306,6 +25373,7 @@
|
|||||||
"version": "11.0.5",
|
"version": "11.0.5",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/unist": "^3.0.0",
|
"@types/unist": "^3.0.0",
|
||||||
"bail": "^2.0.0",
|
"bail": "^2.0.0",
|
||||||
@@ -25748,6 +25816,7 @@
|
|||||||
"version": "5.4.20",
|
"version": "5.4.20",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.21.3",
|
"esbuild": "^0.21.3",
|
||||||
"postcss": "^8.4.43",
|
"postcss": "^8.4.43",
|
||||||
@@ -25860,7 +25929,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
@@ -26587,6 +26655,7 @@
|
|||||||
"node_modules/zod": {
|
"node_modules/zod": {
|
||||||
"version": "4.1.11",
|
"version": "4.1.11",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
@@ -26997,19 +27066,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/ai-sdk-provider-grok-cli/node_modules/yaml": {
|
|
||||||
"version": "2.8.1",
|
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
|
||||||
"optional": true,
|
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
|
||||||
"yaml": "bin.mjs"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 14.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"packages/build-config": {
|
"packages/build-config": {
|
||||||
"name": "@tm/build-config",
|
"name": "@tm/build-config",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -27341,6 +27397,7 @@
|
|||||||
"version": "3.2.4",
|
"version": "3.2.4",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/chai": "^5.2.2",
|
"@types/chai": "^5.2.2",
|
||||||
"@vitest/expect": "3.2.4",
|
"@vitest/expect": "3.2.4",
|
||||||
@@ -27480,26 +27537,6 @@
|
|||||||
"optional": true
|
"optional": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"packages/tm-core/node_modules/yaml": {
|
|
||||||
"version": "2.8.1",
|
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
|
||||||
"optional": true,
|
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
|
||||||
"yaml": "bin.mjs"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 14.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"packages/tm-core/node_modules/zod": {
|
|
||||||
"version": "3.25.76",
|
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "task-master-ai",
|
"name": "task-master-ai",
|
||||||
"version": "0.27.3",
|
"version": "0.28.0-rc.1",
|
||||||
"description": "A task management system for ambitious AI-driven development that doesn't overwhelm and confuse Cursor.",
|
"description": "A task management system for ambitious AI-driven development that doesn't overwhelm and confuse Cursor.",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -71,6 +71,7 @@
|
|||||||
"@supabase/supabase-js": "^2.57.4",
|
"@supabase/supabase-js": "^2.57.4",
|
||||||
"ai": "^5.0.51",
|
"ai": "^5.0.51",
|
||||||
"ai-sdk-provider-claude-code": "^1.1.4",
|
"ai-sdk-provider-claude-code": "^1.1.4",
|
||||||
|
"ai-sdk-provider-codex-cli": "^0.3.0",
|
||||||
"ai-sdk-provider-gemini-cli": "^1.1.1",
|
"ai-sdk-provider-gemini-cli": "^1.1.1",
|
||||||
"ajv": "^8.17.1",
|
"ajv": "^8.17.1",
|
||||||
"ajv-formats": "^3.0.1",
|
"ajv-formats": "^3.0.1",
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ export class TaskEntity implements Task {
|
|||||||
tags?: string[];
|
tags?: string[];
|
||||||
assignee?: string;
|
assignee?: string;
|
||||||
complexity?: Task['complexity'];
|
complexity?: Task['complexity'];
|
||||||
|
recommendedSubtasks?: number;
|
||||||
|
expansionPrompt?: string;
|
||||||
|
complexityReasoning?: string;
|
||||||
|
|
||||||
constructor(data: Task | (Omit<Task, 'id'> & { id: number | string })) {
|
constructor(data: Task | (Omit<Task, 'id'> & { id: number | string })) {
|
||||||
this.validate(data);
|
this.validate(data);
|
||||||
@@ -62,6 +65,9 @@ export class TaskEntity implements Task {
|
|||||||
this.tags = data.tags;
|
this.tags = data.tags;
|
||||||
this.assignee = data.assignee;
|
this.assignee = data.assignee;
|
||||||
this.complexity = data.complexity;
|
this.complexity = data.complexity;
|
||||||
|
this.recommendedSubtasks = data.recommendedSubtasks;
|
||||||
|
this.expansionPrompt = data.expansionPrompt;
|
||||||
|
this.complexityReasoning = data.complexityReasoning;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -246,7 +252,10 @@ export class TaskEntity implements Task {
|
|||||||
actualEffort: this.actualEffort,
|
actualEffort: this.actualEffort,
|
||||||
tags: this.tags,
|
tags: this.tags,
|
||||||
assignee: this.assignee,
|
assignee: this.assignee,
|
||||||
complexity: this.complexity
|
complexity: this.complexity,
|
||||||
|
recommendedSubtasks: this.recommendedSubtasks,
|
||||||
|
expansionPrompt: this.expansionPrompt,
|
||||||
|
complexityReasoning: this.complexityReasoning
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -61,3 +61,12 @@ export { getLogger, createLogger, setGlobalLogger } from './logger/index.js';
|
|||||||
|
|
||||||
// Re-export executors
|
// Re-export executors
|
||||||
export * from './executors/index.js';
|
export * from './executors/index.js';
|
||||||
|
|
||||||
|
// Re-export reports
|
||||||
|
export {
|
||||||
|
ComplexityReportManager,
|
||||||
|
type ComplexityReport,
|
||||||
|
type ComplexityReportMetadata,
|
||||||
|
type ComplexityAnalysis,
|
||||||
|
type TaskComplexityData
|
||||||
|
} from './reports/index.js';
|
||||||
|
|||||||
185
packages/tm-core/src/reports/complexity-report-manager.ts
Normal file
185
packages/tm-core/src/reports/complexity-report-manager.ts
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview ComplexityReportManager - Handles loading and managing complexity analysis reports
|
||||||
|
* Follows the same pattern as ConfigManager and AuthManager
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { promises as fs } from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import type {
|
||||||
|
ComplexityReport,
|
||||||
|
ComplexityAnalysis,
|
||||||
|
TaskComplexityData
|
||||||
|
} from './types.js';
|
||||||
|
import { getLogger } from '../logger/index.js';
|
||||||
|
|
||||||
|
const logger = getLogger('ComplexityReportManager');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages complexity analysis reports
|
||||||
|
* Handles loading, caching, and providing complexity data for tasks
|
||||||
|
*/
|
||||||
|
export class ComplexityReportManager {
|
||||||
|
private projectRoot: string;
|
||||||
|
private reportCache: Map<string, ComplexityReport> = new Map();
|
||||||
|
|
||||||
|
constructor(projectRoot: string) {
|
||||||
|
this.projectRoot = projectRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the path to the complexity report file for a given tag
|
||||||
|
*/
|
||||||
|
private getReportPath(tag?: string): string {
|
||||||
|
const reportsDir = path.join(this.projectRoot, '.taskmaster', 'reports');
|
||||||
|
const tagSuffix = tag && tag !== 'master' ? `_${tag}` : '';
|
||||||
|
return path.join(reportsDir, `task-complexity-report${tagSuffix}.json`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load complexity report for a given tag
|
||||||
|
* Results are cached to avoid repeated file reads
|
||||||
|
*/
|
||||||
|
async loadReport(tag?: string): Promise<ComplexityReport | null> {
|
||||||
|
const resolvedTag = tag || 'master';
|
||||||
|
const cacheKey = resolvedTag;
|
||||||
|
|
||||||
|
// Check cache first
|
||||||
|
if (this.reportCache.has(cacheKey)) {
|
||||||
|
return this.reportCache.get(cacheKey)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reportPath = this.getReportPath(tag);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if file exists
|
||||||
|
await fs.access(reportPath);
|
||||||
|
|
||||||
|
// Read and parse the report
|
||||||
|
const content = await fs.readFile(reportPath, 'utf-8');
|
||||||
|
const report = JSON.parse(content) as ComplexityReport;
|
||||||
|
|
||||||
|
// Validate basic structure
|
||||||
|
if (!report.meta || !Array.isArray(report.complexityAnalysis)) {
|
||||||
|
logger.warn(
|
||||||
|
`Invalid complexity report structure at ${reportPath}, ignoring`
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache the report
|
||||||
|
this.reportCache.set(cacheKey, report);
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
`Loaded complexity report for tag '${resolvedTag}' with ${report.complexityAnalysis.length} analyses`
|
||||||
|
);
|
||||||
|
|
||||||
|
return report;
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.code === 'ENOENT') {
|
||||||
|
// File doesn't exist - this is normal, not all projects have complexity reports
|
||||||
|
logger.debug(`No complexity report found for tag '${resolvedTag}'`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Other errors (parsing, permissions, etc.)
|
||||||
|
logger.warn(
|
||||||
|
`Failed to load complexity report for tag '${resolvedTag}': ${error.message}`
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get complexity data for a specific task ID
|
||||||
|
*/
|
||||||
|
async getComplexityForTask(
|
||||||
|
taskId: string | number,
|
||||||
|
tag?: string
|
||||||
|
): Promise<TaskComplexityData | null> {
|
||||||
|
const report = await this.loadReport(tag);
|
||||||
|
if (!report) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the analysis for this task
|
||||||
|
const analysis = report.complexityAnalysis.find(
|
||||||
|
(a) => String(a.taskId) === String(taskId)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!analysis) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to TaskComplexityData format
|
||||||
|
return {
|
||||||
|
complexityScore: analysis.complexityScore,
|
||||||
|
recommendedSubtasks: analysis.recommendedSubtasks,
|
||||||
|
expansionPrompt: analysis.expansionPrompt,
|
||||||
|
complexityReasoning: analysis.complexityReasoning
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get complexity data for multiple tasks at once
|
||||||
|
* More efficient than calling getComplexityForTask multiple times
|
||||||
|
*/
|
||||||
|
async getComplexityForTasks(
|
||||||
|
taskIds: (string | number)[],
|
||||||
|
tag?: string
|
||||||
|
): Promise<Map<string, TaskComplexityData>> {
|
||||||
|
const result = new Map<string, TaskComplexityData>();
|
||||||
|
const report = await this.loadReport(tag);
|
||||||
|
|
||||||
|
if (!report) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a map for fast lookups
|
||||||
|
const analysisMap = new Map<string, ComplexityAnalysis>();
|
||||||
|
report.complexityAnalysis.forEach((analysis) => {
|
||||||
|
analysisMap.set(String(analysis.taskId), analysis);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Map each task ID to its complexity data
|
||||||
|
taskIds.forEach((taskId) => {
|
||||||
|
const analysis = analysisMap.get(String(taskId));
|
||||||
|
if (analysis) {
|
||||||
|
result.set(String(taskId), {
|
||||||
|
complexityScore: analysis.complexityScore,
|
||||||
|
recommendedSubtasks: analysis.recommendedSubtasks,
|
||||||
|
expansionPrompt: analysis.expansionPrompt,
|
||||||
|
complexityReasoning: analysis.complexityReasoning
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the report cache
|
||||||
|
* @param tag - Specific tag to clear, or undefined to clear all cached reports
|
||||||
|
* Useful when reports are regenerated or modified externally
|
||||||
|
*/
|
||||||
|
clearCache(tag?: string): void {
|
||||||
|
if (tag) {
|
||||||
|
this.reportCache.delete(tag);
|
||||||
|
} else {
|
||||||
|
// Clear all cached reports
|
||||||
|
this.reportCache.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a complexity report exists for a tag
|
||||||
|
*/
|
||||||
|
async hasReport(tag?: string): Promise<boolean> {
|
||||||
|
const reportPath = this.getReportPath(tag);
|
||||||
|
try {
|
||||||
|
await fs.access(reportPath);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
packages/tm-core/src/reports/index.ts
Normal file
11
packages/tm-core/src/reports/index.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Reports module exports
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { ComplexityReportManager } from './complexity-report-manager.js';
|
||||||
|
export type {
|
||||||
|
ComplexityReport,
|
||||||
|
ComplexityReportMetadata,
|
||||||
|
ComplexityAnalysis,
|
||||||
|
TaskComplexityData
|
||||||
|
} from './types.js';
|
||||||
65
packages/tm-core/src/reports/types.ts
Normal file
65
packages/tm-core/src/reports/types.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Type definitions for complexity analysis reports
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Analysis result for a single task
|
||||||
|
*/
|
||||||
|
export interface ComplexityAnalysis {
|
||||||
|
/** Task ID being analyzed */
|
||||||
|
taskId: string | number;
|
||||||
|
/** Task title */
|
||||||
|
taskTitle: string;
|
||||||
|
/** Complexity score (1-10 scale) */
|
||||||
|
complexityScore: number;
|
||||||
|
/** Recommended number of subtasks */
|
||||||
|
recommendedSubtasks: number;
|
||||||
|
/** AI-generated prompt for task expansion */
|
||||||
|
expansionPrompt: string;
|
||||||
|
/** Reasoning behind the complexity assessment */
|
||||||
|
complexityReasoning: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metadata about the complexity report
|
||||||
|
*/
|
||||||
|
export interface ComplexityReportMetadata {
|
||||||
|
/** When the report was generated */
|
||||||
|
generatedAt: string;
|
||||||
|
/** Number of tasks analyzed in this run */
|
||||||
|
tasksAnalyzed: number;
|
||||||
|
/** Total number of tasks in the file */
|
||||||
|
totalTasks?: number;
|
||||||
|
/** Total analyses in the report (across all runs) */
|
||||||
|
analysisCount?: number;
|
||||||
|
/** Complexity threshold score used */
|
||||||
|
thresholdScore: number;
|
||||||
|
/** Project name */
|
||||||
|
projectName?: string;
|
||||||
|
/** Whether research mode was used */
|
||||||
|
usedResearch: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Complete complexity analysis report
|
||||||
|
*/
|
||||||
|
export interface ComplexityReport {
|
||||||
|
/** Report metadata */
|
||||||
|
meta: ComplexityReportMetadata;
|
||||||
|
/** Array of complexity analyses */
|
||||||
|
complexityAnalysis: ComplexityAnalysis[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Complexity data to be attached to a Task
|
||||||
|
*/
|
||||||
|
export interface TaskComplexityData {
|
||||||
|
/** Complexity score (1-10 scale) */
|
||||||
|
complexityScore?: number;
|
||||||
|
/** Recommended number of subtasks */
|
||||||
|
recommendedSubtasks?: number;
|
||||||
|
/** AI-generated expansion prompt */
|
||||||
|
expansionPrompt?: string;
|
||||||
|
/** Reasoning behind the assessment */
|
||||||
|
complexityReasoning?: string;
|
||||||
|
}
|
||||||
@@ -162,7 +162,7 @@ export class SupabaseTaskRepository {
|
|||||||
TaskUpdateSchema.parse(updates);
|
TaskUpdateSchema.parse(updates);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof z.ZodError) {
|
if (error instanceof z.ZodError) {
|
||||||
const errorMessages = error.errors
|
const errorMessages = error.issues
|
||||||
.map((err) => `${err.path.join('.')}: ${err.message}`)
|
.map((err) => `${err.path.join('.')}: ${err.message}`)
|
||||||
.join(', ');
|
.join(', ');
|
||||||
throw new Error(`Invalid task update data: ${errorMessages}`);
|
throw new Error(`Invalid task update data: ${errorMessages}`);
|
||||||
|
|||||||
@@ -397,16 +397,6 @@ export class TaskService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Complexity filter
|
|
||||||
if (filter.complexity) {
|
|
||||||
const complexities = Array.isArray(filter.complexity)
|
|
||||||
? filter.complexity
|
|
||||||
: [filter.complexity];
|
|
||||||
if (!task.complexity || !complexities.includes(task.complexity)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search filter
|
// Search filter
|
||||||
if (filter.search) {
|
if (filter.search) {
|
||||||
const searchLower = filter.search.toLowerCase();
|
const searchLower = filter.search.toLowerCase();
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import type {
|
|||||||
import { FormatHandler } from './format-handler.js';
|
import { FormatHandler } from './format-handler.js';
|
||||||
import { FileOperations } from './file-operations.js';
|
import { FileOperations } from './file-operations.js';
|
||||||
import { PathResolver } from './path-resolver.js';
|
import { PathResolver } from './path-resolver.js';
|
||||||
|
import { ComplexityReportManager } from '../../reports/complexity-report-manager.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* File-based storage implementation using a single tasks.json file with separated concerns
|
* File-based storage implementation using a single tasks.json file with separated concerns
|
||||||
@@ -19,11 +20,13 @@ export class FileStorage implements IStorage {
|
|||||||
private formatHandler: FormatHandler;
|
private formatHandler: FormatHandler;
|
||||||
private fileOps: FileOperations;
|
private fileOps: FileOperations;
|
||||||
private pathResolver: PathResolver;
|
private pathResolver: PathResolver;
|
||||||
|
private complexityManager: ComplexityReportManager;
|
||||||
|
|
||||||
constructor(projectPath: string) {
|
constructor(projectPath: string) {
|
||||||
this.formatHandler = new FormatHandler();
|
this.formatHandler = new FormatHandler();
|
||||||
this.fileOps = new FileOperations();
|
this.fileOps = new FileOperations();
|
||||||
this.pathResolver = new PathResolver(projectPath);
|
this.pathResolver = new PathResolver(projectPath);
|
||||||
|
this.complexityManager = new ComplexityReportManager(projectPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -87,6 +90,7 @@ export class FileStorage implements IStorage {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Load tasks from the single tasks.json file for a specific tag
|
* Load tasks from the single tasks.json file for a specific tag
|
||||||
|
* Enriches tasks with complexity data from the complexity report
|
||||||
*/
|
*/
|
||||||
async loadTasks(tag?: string): Promise<Task[]> {
|
async loadTasks(tag?: string): Promise<Task[]> {
|
||||||
const filePath = this.pathResolver.getTasksPath();
|
const filePath = this.pathResolver.getTasksPath();
|
||||||
@@ -94,7 +98,10 @@ export class FileStorage implements IStorage {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const rawData = await this.fileOps.readJson(filePath);
|
const rawData = await this.fileOps.readJson(filePath);
|
||||||
return this.formatHandler.extractTasks(rawData, resolvedTag);
|
const tasks = this.formatHandler.extractTasks(rawData, resolvedTag);
|
||||||
|
|
||||||
|
// Enrich tasks with complexity data
|
||||||
|
return await this.enrichTasksWithComplexity(tasks, resolvedTag);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (error.code === 'ENOENT') {
|
if (error.code === 'ENOENT') {
|
||||||
return []; // File doesn't exist, return empty array
|
return []; // File doesn't exist, return empty array
|
||||||
@@ -596,6 +603,46 @@ export class FileStorage implements IStorage {
|
|||||||
|
|
||||||
await this.saveTasks(tasks, targetTag);
|
await this.saveTasks(tasks, targetTag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enrich tasks with complexity data from the complexity report
|
||||||
|
* Private helper method called by loadTasks()
|
||||||
|
*/
|
||||||
|
private async enrichTasksWithComplexity(
|
||||||
|
tasks: Task[],
|
||||||
|
tag: string
|
||||||
|
): Promise<Task[]> {
|
||||||
|
// Get all task IDs for bulk lookup
|
||||||
|
const taskIds = tasks.map((t) => t.id);
|
||||||
|
|
||||||
|
// Load complexity data for all tasks at once (more efficient)
|
||||||
|
const complexityMap = await this.complexityManager.getComplexityForTasks(
|
||||||
|
taskIds,
|
||||||
|
tag
|
||||||
|
);
|
||||||
|
|
||||||
|
// If no complexity data found, return tasks as-is
|
||||||
|
if (complexityMap.size === 0) {
|
||||||
|
return tasks;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enrich each task with its complexity data
|
||||||
|
return tasks.map((task) => {
|
||||||
|
const complexityData = complexityMap.get(String(task.id));
|
||||||
|
if (!complexityData) {
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge complexity data into the task
|
||||||
|
return {
|
||||||
|
...task,
|
||||||
|
complexity: complexityData.complexityScore,
|
||||||
|
recommendedSubtasks: complexityData.recommendedSubtasks,
|
||||||
|
expansionPrompt: complexityData.expansionPrompt,
|
||||||
|
complexityReasoning: complexityData.complexityReasoning
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export as default for convenience
|
// Export as default for convenience
|
||||||
|
|||||||
@@ -72,7 +72,13 @@ export interface Task {
|
|||||||
actualEffort?: number;
|
actualEffort?: number;
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
assignee?: string;
|
assignee?: string;
|
||||||
complexity?: TaskComplexity;
|
|
||||||
|
// Complexity analysis (from complexity report)
|
||||||
|
// Can be either enum ('simple' | 'moderate' | 'complex' | 'very-complex') or numeric score (1-10)
|
||||||
|
complexity?: TaskComplexity | number;
|
||||||
|
recommendedSubtasks?: number;
|
||||||
|
expansionPrompt?: string;
|
||||||
|
complexityReasoning?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -145,7 +151,6 @@ export interface TaskFilter {
|
|||||||
hasSubtasks?: boolean;
|
hasSubtasks?: boolean;
|
||||||
search?: string;
|
search?: string;
|
||||||
assignee?: string;
|
assignee?: string;
|
||||||
complexity?: TaskComplexity | TaskComplexity[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import {
|
|||||||
AzureProvider,
|
AzureProvider,
|
||||||
BedrockAIProvider,
|
BedrockAIProvider,
|
||||||
ClaudeCodeProvider,
|
ClaudeCodeProvider,
|
||||||
|
CodexCliProvider,
|
||||||
GeminiCliProvider,
|
GeminiCliProvider,
|
||||||
GoogleAIProvider,
|
GoogleAIProvider,
|
||||||
GrokCliProvider,
|
GrokCliProvider,
|
||||||
@@ -70,6 +71,7 @@ const PROVIDERS = {
|
|||||||
azure: new AzureProvider(),
|
azure: new AzureProvider(),
|
||||||
vertex: new VertexAIProvider(),
|
vertex: new VertexAIProvider(),
|
||||||
'claude-code': new ClaudeCodeProvider(),
|
'claude-code': new ClaudeCodeProvider(),
|
||||||
|
'codex-cli': new CodexCliProvider(),
|
||||||
'gemini-cli': new GeminiCliProvider(),
|
'gemini-cli': new GeminiCliProvider(),
|
||||||
'grok-cli': new GrokCliProvider()
|
'grok-cli': new GrokCliProvider()
|
||||||
};
|
};
|
||||||
@@ -93,31 +95,55 @@ function _getProvider(providerName) {
|
|||||||
|
|
||||||
// Helper function to get cost for a specific model
|
// Helper function to get cost for a specific model
|
||||||
function _getCostForModel(providerName, modelId) {
|
function _getCostForModel(providerName, modelId) {
|
||||||
const DEFAULT_COST = { inputCost: 0, outputCost: 0, currency: 'USD' };
|
const DEFAULT_COST = {
|
||||||
|
inputCost: 0,
|
||||||
|
outputCost: 0,
|
||||||
|
currency: 'USD',
|
||||||
|
isUnknown: false
|
||||||
|
};
|
||||||
|
|
||||||
if (!MODEL_MAP || !MODEL_MAP[providerName]) {
|
if (!MODEL_MAP || !MODEL_MAP[providerName]) {
|
||||||
log(
|
log(
|
||||||
'warn',
|
'warn',
|
||||||
`Provider "${providerName}" not found in MODEL_MAP. Cannot determine cost for model ${modelId}.`
|
`Provider "${providerName}" not found in MODEL_MAP. Cannot determine cost for model ${modelId}.`
|
||||||
);
|
);
|
||||||
return DEFAULT_COST;
|
return { ...DEFAULT_COST, isUnknown: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
const modelData = MODEL_MAP[providerName].find((m) => m.id === modelId);
|
const modelData = MODEL_MAP[providerName].find((m) => m.id === modelId);
|
||||||
|
|
||||||
if (!modelData?.cost_per_1m_tokens) {
|
if (!modelData) {
|
||||||
log(
|
log(
|
||||||
'debug',
|
'debug',
|
||||||
`Cost data not found for model "${modelId}" under provider "${providerName}". Assuming zero cost.`
|
`Model "${modelId}" not found under provider "${providerName}". Assuming unknown cost.`
|
||||||
);
|
);
|
||||||
return DEFAULT_COST;
|
return { ...DEFAULT_COST, isUnknown: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if cost_per_1m_tokens is explicitly null (unknown pricing)
|
||||||
|
if (modelData.cost_per_1m_tokens === null) {
|
||||||
|
log(
|
||||||
|
'debug',
|
||||||
|
`Cost data is null for model "${modelId}" under provider "${providerName}". Pricing unknown.`
|
||||||
|
);
|
||||||
|
return { ...DEFAULT_COST, isUnknown: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if cost_per_1m_tokens is missing/undefined (also unknown)
|
||||||
|
if (modelData.cost_per_1m_tokens === undefined) {
|
||||||
|
log(
|
||||||
|
'debug',
|
||||||
|
`Cost data not found for model "${modelId}" under provider "${providerName}". Pricing unknown.`
|
||||||
|
);
|
||||||
|
return { ...DEFAULT_COST, isUnknown: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
const costs = modelData.cost_per_1m_tokens;
|
const costs = modelData.cost_per_1m_tokens;
|
||||||
return {
|
return {
|
||||||
inputCost: costs.input || 0,
|
inputCost: costs.input || 0,
|
||||||
outputCost: costs.output || 0,
|
outputCost: costs.output || 0,
|
||||||
currency: costs.currency || 'USD'
|
currency: costs.currency || 'USD',
|
||||||
|
isUnknown: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -867,8 +893,8 @@ async function logAiUsage({
|
|||||||
const timestamp = new Date().toISOString();
|
const timestamp = new Date().toISOString();
|
||||||
const totalTokens = (inputTokens || 0) + (outputTokens || 0);
|
const totalTokens = (inputTokens || 0) + (outputTokens || 0);
|
||||||
|
|
||||||
// Destructure currency along with costs
|
// Destructure currency along with costs and unknown flag
|
||||||
const { inputCost, outputCost, currency } = _getCostForModel(
|
const { inputCost, outputCost, currency, isUnknown } = _getCostForModel(
|
||||||
providerName,
|
providerName,
|
||||||
modelId
|
modelId
|
||||||
);
|
);
|
||||||
@@ -890,7 +916,8 @@ async function logAiUsage({
|
|||||||
outputTokens: outputTokens || 0,
|
outputTokens: outputTokens || 0,
|
||||||
totalTokens,
|
totalTokens,
|
||||||
totalCost,
|
totalCost,
|
||||||
currency // Add currency to the telemetry data
|
currency, // Add currency to the telemetry data
|
||||||
|
isUnknownCost: isUnknown // Flag to indicate if pricing is unknown
|
||||||
};
|
};
|
||||||
|
|
||||||
if (getDebugFlag()) {
|
if (getDebugFlag()) {
|
||||||
|
|||||||
@@ -3586,6 +3586,10 @@ ${result.result}
|
|||||||
'--gemini-cli',
|
'--gemini-cli',
|
||||||
'Allow setting a Gemini CLI model ID (use with --set-*)'
|
'Allow setting a Gemini CLI model ID (use with --set-*)'
|
||||||
)
|
)
|
||||||
|
.option(
|
||||||
|
'--codex-cli',
|
||||||
|
'Allow setting a Codex CLI model ID (use with --set-*)'
|
||||||
|
)
|
||||||
.addHelpText(
|
.addHelpText(
|
||||||
'after',
|
'after',
|
||||||
`
|
`
|
||||||
@@ -3601,6 +3605,7 @@ Examples:
|
|||||||
$ task-master models --set-main gpt-4o --azure # Set custom Azure OpenAI model for main role
|
$ task-master models --set-main gpt-4o --azure # Set custom Azure OpenAI model for main role
|
||||||
$ task-master models --set-main claude-3-5-sonnet@20241022 --vertex # Set custom Vertex AI model for main role
|
$ task-master models --set-main claude-3-5-sonnet@20241022 --vertex # Set custom Vertex AI model for main role
|
||||||
$ task-master models --set-main gemini-2.5-pro --gemini-cli # Set Gemini CLI model for main role
|
$ task-master models --set-main gemini-2.5-pro --gemini-cli # Set Gemini CLI model for main role
|
||||||
|
$ task-master models --set-main gpt-5-codex --codex-cli # Set Codex CLI model for main role
|
||||||
$ task-master models --setup # Run interactive setup`
|
$ task-master models --setup # Run interactive setup`
|
||||||
)
|
)
|
||||||
.action(async (options) => {
|
.action(async (options) => {
|
||||||
@@ -3617,12 +3622,13 @@ Examples:
|
|||||||
options.ollama,
|
options.ollama,
|
||||||
options.bedrock,
|
options.bedrock,
|
||||||
options.claudeCode,
|
options.claudeCode,
|
||||||
options.geminiCli
|
options.geminiCli,
|
||||||
|
options.codexCli
|
||||||
].filter(Boolean).length;
|
].filter(Boolean).length;
|
||||||
if (providerFlags > 1) {
|
if (providerFlags > 1) {
|
||||||
console.error(
|
console.error(
|
||||||
chalk.red(
|
chalk.red(
|
||||||
'Error: Cannot use multiple provider flags (--openrouter, --ollama, --bedrock, --claude-code, --gemini-cli) simultaneously.'
|
'Error: Cannot use multiple provider flags (--openrouter, --ollama, --bedrock, --claude-code, --gemini-cli, --codex-cli) simultaneously.'
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
@@ -3668,7 +3674,9 @@ Examples:
|
|||||||
? 'claude-code'
|
? 'claude-code'
|
||||||
: options.geminiCli
|
: options.geminiCli
|
||||||
? 'gemini-cli'
|
? 'gemini-cli'
|
||||||
: undefined
|
: options.codexCli
|
||||||
|
? 'codex-cli'
|
||||||
|
: undefined
|
||||||
});
|
});
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
console.log(chalk.green(`✅ ${result.data.message}`));
|
console.log(chalk.green(`✅ ${result.data.message}`));
|
||||||
@@ -3694,7 +3702,9 @@ Examples:
|
|||||||
? 'claude-code'
|
? 'claude-code'
|
||||||
: options.geminiCli
|
: options.geminiCli
|
||||||
? 'gemini-cli'
|
? 'gemini-cli'
|
||||||
: undefined
|
: options.codexCli
|
||||||
|
? 'codex-cli'
|
||||||
|
: undefined
|
||||||
});
|
});
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
console.log(chalk.green(`✅ ${result.data.message}`));
|
console.log(chalk.green(`✅ ${result.data.message}`));
|
||||||
@@ -3722,7 +3732,9 @@ Examples:
|
|||||||
? 'claude-code'
|
? 'claude-code'
|
||||||
: options.geminiCli
|
: options.geminiCli
|
||||||
? 'gemini-cli'
|
? 'gemini-cli'
|
||||||
: undefined
|
: options.codexCli
|
||||||
|
? 'codex-cli'
|
||||||
|
: undefined
|
||||||
});
|
});
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
console.log(chalk.green(`✅ ${result.data.message}`));
|
console.log(chalk.green(`✅ ${result.data.message}`));
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ const DEFAULTS = {
|
|||||||
enableCodebaseAnalysis: true
|
enableCodebaseAnalysis: true
|
||||||
},
|
},
|
||||||
claudeCode: {},
|
claudeCode: {},
|
||||||
|
codexCli: {},
|
||||||
grokCli: {
|
grokCli: {
|
||||||
timeout: 120000,
|
timeout: 120000,
|
||||||
workingDirectory: null,
|
workingDirectory: null,
|
||||||
@@ -138,6 +139,7 @@ function _loadAndValidateConfig(explicitRoot = null) {
|
|||||||
},
|
},
|
||||||
global: { ...defaults.global, ...parsedConfig?.global },
|
global: { ...defaults.global, ...parsedConfig?.global },
|
||||||
claudeCode: { ...defaults.claudeCode, ...parsedConfig?.claudeCode },
|
claudeCode: { ...defaults.claudeCode, ...parsedConfig?.claudeCode },
|
||||||
|
codexCli: { ...defaults.codexCli, ...parsedConfig?.codexCli },
|
||||||
grokCli: { ...defaults.grokCli, ...parsedConfig?.grokCli }
|
grokCli: { ...defaults.grokCli, ...parsedConfig?.grokCli }
|
||||||
};
|
};
|
||||||
configSource = `file (${configPath})`; // Update source info
|
configSource = `file (${configPath})`; // Update source info
|
||||||
@@ -184,6 +186,9 @@ function _loadAndValidateConfig(explicitRoot = null) {
|
|||||||
if (config.claudeCode && !isEmpty(config.claudeCode)) {
|
if (config.claudeCode && !isEmpty(config.claudeCode)) {
|
||||||
config.claudeCode = validateClaudeCodeSettings(config.claudeCode);
|
config.claudeCode = validateClaudeCodeSettings(config.claudeCode);
|
||||||
}
|
}
|
||||||
|
if (config.codexCli && !isEmpty(config.codexCli)) {
|
||||||
|
config.codexCli = validateCodexCliSettings(config.codexCli);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Use console.error for actual errors during parsing
|
// Use console.error for actual errors during parsing
|
||||||
console.error(
|
console.error(
|
||||||
@@ -366,6 +371,57 @@ function validateClaudeCodeSettings(settings) {
|
|||||||
return validatedSettings;
|
return validatedSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates Codex CLI provider custom settings
|
||||||
|
* Mirrors the ai-sdk-provider-codex-cli options
|
||||||
|
* @param {object} settings The settings to validate
|
||||||
|
* @returns {object} The validated settings
|
||||||
|
*/
|
||||||
|
function validateCodexCliSettings(settings) {
|
||||||
|
const BaseSettingsSchema = z.object({
|
||||||
|
codexPath: z.string().optional(),
|
||||||
|
cwd: z.string().optional(),
|
||||||
|
approvalMode: z
|
||||||
|
.enum(['untrusted', 'on-failure', 'on-request', 'never'])
|
||||||
|
.optional(),
|
||||||
|
sandboxMode: z
|
||||||
|
.enum(['read-only', 'workspace-write', 'danger-full-access'])
|
||||||
|
.optional(),
|
||||||
|
fullAuto: z.boolean().optional(),
|
||||||
|
dangerouslyBypassApprovalsAndSandbox: z.boolean().optional(),
|
||||||
|
skipGitRepoCheck: z.boolean().optional(),
|
||||||
|
color: z.enum(['always', 'never', 'auto']).optional(),
|
||||||
|
allowNpx: z.boolean().optional(),
|
||||||
|
outputLastMessageFile: z.string().optional(),
|
||||||
|
env: z.record(z.string(), z.string()).optional(),
|
||||||
|
verbose: z.boolean().optional(),
|
||||||
|
logger: z.union([z.object({}).passthrough(), z.literal(false)]).optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
const CommandSpecificSchema = z
|
||||||
|
.record(z.string(), BaseSettingsSchema)
|
||||||
|
.refine(
|
||||||
|
(obj) =>
|
||||||
|
Object.keys(obj || {}).every((k) => AI_COMMAND_NAMES.includes(k)),
|
||||||
|
{ message: 'Invalid command name in commandSpecific' }
|
||||||
|
);
|
||||||
|
|
||||||
|
const SettingsSchema = BaseSettingsSchema.extend({
|
||||||
|
commandSpecific: CommandSpecificSchema.optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
return SettingsSchema.parse(settings);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(
|
||||||
|
chalk.yellow(
|
||||||
|
`Warning: Invalid Codex CLI settings in config: ${error.message}. Falling back to default.`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Claude Code Settings Getters ---
|
// --- Claude Code Settings Getters ---
|
||||||
|
|
||||||
function getClaudeCodeSettings(explicitRoot = null, forceReload = false) {
|
function getClaudeCodeSettings(explicitRoot = null, forceReload = false) {
|
||||||
@@ -374,6 +430,23 @@ function getClaudeCodeSettings(explicitRoot = null, forceReload = false) {
|
|||||||
return { ...DEFAULTS.claudeCode, ...(config?.claudeCode || {}) };
|
return { ...DEFAULTS.claudeCode, ...(config?.claudeCode || {}) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Codex CLI Settings Getters ---
|
||||||
|
|
||||||
|
function getCodexCliSettings(explicitRoot = null, forceReload = false) {
|
||||||
|
const config = getConfig(explicitRoot, forceReload);
|
||||||
|
return { ...DEFAULTS.codexCli, ...(config?.codexCli || {}) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCodexCliSettingsForCommand(
|
||||||
|
commandName,
|
||||||
|
explicitRoot = null,
|
||||||
|
forceReload = false
|
||||||
|
) {
|
||||||
|
const settings = getCodexCliSettings(explicitRoot, forceReload);
|
||||||
|
const commandSpecific = settings?.commandSpecific || {};
|
||||||
|
return { ...settings, ...commandSpecific[commandName] };
|
||||||
|
}
|
||||||
|
|
||||||
function getClaudeCodeSettingsForCommand(
|
function getClaudeCodeSettingsForCommand(
|
||||||
commandName,
|
commandName,
|
||||||
explicitRoot = null,
|
explicitRoot = null,
|
||||||
@@ -491,7 +564,8 @@ function hasCodebaseAnalysis(
|
|||||||
return (
|
return (
|
||||||
currentProvider === CUSTOM_PROVIDERS.CLAUDE_CODE ||
|
currentProvider === CUSTOM_PROVIDERS.CLAUDE_CODE ||
|
||||||
currentProvider === CUSTOM_PROVIDERS.GEMINI_CLI ||
|
currentProvider === CUSTOM_PROVIDERS.GEMINI_CLI ||
|
||||||
currentProvider === CUSTOM_PROVIDERS.GROK_CLI
|
currentProvider === CUSTOM_PROVIDERS.GROK_CLI ||
|
||||||
|
currentProvider === CUSTOM_PROVIDERS.CODEX_CLI
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -721,7 +795,8 @@ function isApiKeySet(providerName, session = null, projectRoot = null) {
|
|||||||
CUSTOM_PROVIDERS.BEDROCK,
|
CUSTOM_PROVIDERS.BEDROCK,
|
||||||
CUSTOM_PROVIDERS.MCP,
|
CUSTOM_PROVIDERS.MCP,
|
||||||
CUSTOM_PROVIDERS.GEMINI_CLI,
|
CUSTOM_PROVIDERS.GEMINI_CLI,
|
||||||
CUSTOM_PROVIDERS.GROK_CLI
|
CUSTOM_PROVIDERS.GROK_CLI,
|
||||||
|
CUSTOM_PROVIDERS.CODEX_CLI
|
||||||
];
|
];
|
||||||
|
|
||||||
if (providersWithoutApiKeys.includes(providerName?.toLowerCase())) {
|
if (providersWithoutApiKeys.includes(providerName?.toLowerCase())) {
|
||||||
@@ -733,6 +808,11 @@ function isApiKeySet(providerName, session = null, projectRoot = null) {
|
|||||||
return true; // No API key needed
|
return true; // No API key needed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Codex CLI supports OAuth via codex login; API key optional
|
||||||
|
if (providerName?.toLowerCase() === 'codex-cli') {
|
||||||
|
return true; // Treat as OK even without key
|
||||||
|
}
|
||||||
|
|
||||||
const keyMap = {
|
const keyMap = {
|
||||||
openai: 'OPENAI_API_KEY',
|
openai: 'OPENAI_API_KEY',
|
||||||
anthropic: 'ANTHROPIC_API_KEY',
|
anthropic: 'ANTHROPIC_API_KEY',
|
||||||
@@ -836,6 +916,8 @@ function getMcpApiKeyStatus(providerName, projectRoot = null) {
|
|||||||
return true; // No key needed
|
return true; // No key needed
|
||||||
case 'claude-code':
|
case 'claude-code':
|
||||||
return true; // No key needed
|
return true; // No key needed
|
||||||
|
case 'codex-cli':
|
||||||
|
return true; // OAuth/subscription via Codex CLI
|
||||||
case 'mistral':
|
case 'mistral':
|
||||||
apiKeyToCheck = mcpEnv.MISTRAL_API_KEY;
|
apiKeyToCheck = mcpEnv.MISTRAL_API_KEY;
|
||||||
placeholderValue = 'YOUR_MISTRAL_API_KEY_HERE';
|
placeholderValue = 'YOUR_MISTRAL_API_KEY_HERE';
|
||||||
@@ -1028,7 +1110,8 @@ export const providersWithoutApiKeys = [
|
|||||||
CUSTOM_PROVIDERS.BEDROCK,
|
CUSTOM_PROVIDERS.BEDROCK,
|
||||||
CUSTOM_PROVIDERS.GEMINI_CLI,
|
CUSTOM_PROVIDERS.GEMINI_CLI,
|
||||||
CUSTOM_PROVIDERS.GROK_CLI,
|
CUSTOM_PROVIDERS.GROK_CLI,
|
||||||
CUSTOM_PROVIDERS.MCP
|
CUSTOM_PROVIDERS.MCP,
|
||||||
|
CUSTOM_PROVIDERS.CODEX_CLI
|
||||||
];
|
];
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@@ -1040,6 +1123,9 @@ export {
|
|||||||
// Claude Code settings
|
// Claude Code settings
|
||||||
getClaudeCodeSettings,
|
getClaudeCodeSettings,
|
||||||
getClaudeCodeSettingsForCommand,
|
getClaudeCodeSettingsForCommand,
|
||||||
|
// Codex CLI settings
|
||||||
|
getCodexCliSettings,
|
||||||
|
getCodexCliSettingsForCommand,
|
||||||
// Grok CLI settings
|
// Grok CLI settings
|
||||||
getGrokCliSettings,
|
getGrokCliSettings,
|
||||||
getGrokCliSettingsForCommand,
|
getGrokCliSettingsForCommand,
|
||||||
@@ -1047,6 +1133,7 @@ export {
|
|||||||
validateProvider,
|
validateProvider,
|
||||||
validateProviderModelCombination,
|
validateProviderModelCombination,
|
||||||
validateClaudeCodeSettings,
|
validateClaudeCodeSettings,
|
||||||
|
validateCodexCliSettings,
|
||||||
VALIDATED_PROVIDERS,
|
VALIDATED_PROVIDERS,
|
||||||
CUSTOM_PROVIDERS,
|
CUSTOM_PROVIDERS,
|
||||||
ALL_PROVIDERS,
|
ALL_PROVIDERS,
|
||||||
|
|||||||
@@ -69,6 +69,30 @@
|
|||||||
"supported": true
|
"supported": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"codex-cli": [
|
||||||
|
{
|
||||||
|
"id": "gpt-5",
|
||||||
|
"swe_score": 0.749,
|
||||||
|
"cost_per_1m_tokens": {
|
||||||
|
"input": 0,
|
||||||
|
"output": 0
|
||||||
|
},
|
||||||
|
"allowed_roles": ["main", "fallback", "research"],
|
||||||
|
"max_tokens": 128000,
|
||||||
|
"supported": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gpt-5-codex",
|
||||||
|
"swe_score": 0.749,
|
||||||
|
"cost_per_1m_tokens": {
|
||||||
|
"input": 0,
|
||||||
|
"output": 0
|
||||||
|
},
|
||||||
|
"allowed_roles": ["main", "fallback", "research"],
|
||||||
|
"max_tokens": 128000,
|
||||||
|
"supported": true
|
||||||
|
}
|
||||||
|
],
|
||||||
"mcp": [
|
"mcp": [
|
||||||
{
|
{
|
||||||
"id": "mcp-sampling",
|
"id": "mcp-sampling",
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import path from 'path';
|
|
||||||
|
|
||||||
import { log, readJSON, writeJSON, getCurrentTag } from '../utils.js';
|
import { log, readJSON, writeJSON, getCurrentTag } from '../utils.js';
|
||||||
import { isTaskDependentOn } from '../task-manager.js';
|
import { isTaskDependentOn } from '../task-manager.js';
|
||||||
import generateTaskFiles from './generate-task-files.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a subtask to a parent task
|
* Add a subtask to a parent task
|
||||||
@@ -142,11 +139,7 @@ async function addSubtask(
|
|||||||
// Write the updated tasks back to the file with proper context
|
// Write the updated tasks back to the file with proper context
|
||||||
writeJSON(tasksPath, data, projectRoot, tag);
|
writeJSON(tasksPath, data, projectRoot, tag);
|
||||||
|
|
||||||
// Generate task files if requested
|
// Note: Task file generation is no longer supported and has been removed
|
||||||
if (generateFiles) {
|
|
||||||
log('info', 'Regenerating task files...');
|
|
||||||
await generateTaskFiles(tasksPath, path.dirname(tasksPath), context);
|
|
||||||
}
|
|
||||||
|
|
||||||
return newSubtask;
|
return newSubtask;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -539,6 +539,22 @@ async function setModel(role, modelId, options = {}) {
|
|||||||
warningMessage = `Warning: Gemini CLI model '${modelId}' not found in supported models. Setting without validation.`;
|
warningMessage = `Warning: Gemini CLI model '${modelId}' not found in supported models. Setting without validation.`;
|
||||||
report('warn', warningMessage);
|
report('warn', warningMessage);
|
||||||
}
|
}
|
||||||
|
} else if (providerHint === CUSTOM_PROVIDERS.CODEX_CLI) {
|
||||||
|
// Codex CLI provider - enforce supported model list
|
||||||
|
determinedProvider = CUSTOM_PROVIDERS.CODEX_CLI;
|
||||||
|
const codexCliModels = availableModels.filter(
|
||||||
|
(m) => m.provider === 'codex-cli'
|
||||||
|
);
|
||||||
|
const codexCliModelData = codexCliModels.find(
|
||||||
|
(m) => m.id === modelId
|
||||||
|
);
|
||||||
|
if (codexCliModelData) {
|
||||||
|
modelData = codexCliModelData;
|
||||||
|
report('info', `Setting Codex CLI model '${modelId}'.`);
|
||||||
|
} else {
|
||||||
|
warningMessage = `Warning: Codex CLI model '${modelId}' not found in supported models. Setting without validation.`;
|
||||||
|
report('warn', warningMessage);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Invalid provider hint - should not happen with our constants
|
// Invalid provider hint - should not happen with our constants
|
||||||
throw new Error(`Invalid provider hint received: ${providerHint}`);
|
throw new Error(`Invalid provider hint received: ${providerHint}`);
|
||||||
@@ -559,7 +575,7 @@ async function setModel(role, modelId, options = {}) {
|
|||||||
success: false,
|
success: false,
|
||||||
error: {
|
error: {
|
||||||
code: 'MODEL_NOT_FOUND_NO_HINT',
|
code: 'MODEL_NOT_FOUND_NO_HINT',
|
||||||
message: `Model ID "${modelId}" not found in Taskmaster's supported models. If this is a custom model, please specify the provider using --openrouter, --ollama, --bedrock, --azure, or --vertex.`
|
message: `Model ID "${modelId}" not found in Taskmaster's supported models. If this is a custom model, please specify the provider using --openrouter, --ollama, --bedrock, --azure, --vertex, --gemini-cli, or --codex-cli.`
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
setTasksForTag,
|
setTasksForTag,
|
||||||
traverseDependencies
|
traverseDependencies
|
||||||
} from '../utils.js';
|
} from '../utils.js';
|
||||||
import generateTaskFiles from './generate-task-files.js';
|
|
||||||
import {
|
import {
|
||||||
findCrossTagDependencies,
|
findCrossTagDependencies,
|
||||||
getDependentTaskIds,
|
getDependentTaskIds,
|
||||||
@@ -142,13 +141,7 @@ async function moveTask(
|
|||||||
results.push(result);
|
results.push(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate files once at the end if requested
|
// Note: Task file generation is no longer supported and has been removed
|
||||||
if (generateFiles) {
|
|
||||||
await generateTaskFiles(tasksPath, path.dirname(tasksPath), {
|
|
||||||
tag: tag,
|
|
||||||
projectRoot: projectRoot
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
message: `Successfully moved ${sourceIds.length} tasks/subtasks`,
|
message: `Successfully moved ${sourceIds.length} tasks/subtasks`,
|
||||||
@@ -209,12 +202,7 @@ async function moveTask(
|
|||||||
// The writeJSON function will filter out _rawTaggedData automatically
|
// The writeJSON function will filter out _rawTaggedData automatically
|
||||||
writeJSON(tasksPath, rawData, options.projectRoot, tag);
|
writeJSON(tasksPath, rawData, options.projectRoot, tag);
|
||||||
|
|
||||||
if (generateFiles) {
|
// Note: Task file generation is no longer supported and has been removed
|
||||||
await generateTaskFiles(tasksPath, path.dirname(tasksPath), {
|
|
||||||
tag: tag,
|
|
||||||
projectRoot: projectRoot
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import path from 'path';
|
|
||||||
import { log, readJSON, writeJSON } from '../utils.js';
|
import { log, readJSON, writeJSON } from '../utils.js';
|
||||||
import generateTaskFiles from './generate-task-files.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove a subtask from its parent task
|
* Remove a subtask from its parent task
|
||||||
@@ -108,11 +106,7 @@ async function removeSubtask(
|
|||||||
// Write the updated tasks back to the file with proper context
|
// Write the updated tasks back to the file with proper context
|
||||||
writeJSON(tasksPath, data, projectRoot, tag);
|
writeJSON(tasksPath, data, projectRoot, tag);
|
||||||
|
|
||||||
// Generate task files if requested
|
// Note: Task file generation is no longer supported and has been removed
|
||||||
if (generateFiles) {
|
|
||||||
log('info', 'Regenerating task files...');
|
|
||||||
await generateTaskFiles(tasksPath, path.dirname(tasksPath), context);
|
|
||||||
}
|
|
||||||
|
|
||||||
return convertedTask;
|
return convertedTask;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -2310,7 +2310,8 @@ function displayAiUsageSummary(telemetryData, outputType = 'cli') {
|
|||||||
outputTokens,
|
outputTokens,
|
||||||
totalTokens,
|
totalTokens,
|
||||||
totalCost,
|
totalCost,
|
||||||
commandName
|
commandName,
|
||||||
|
isUnknownCost
|
||||||
} = telemetryData;
|
} = telemetryData;
|
||||||
|
|
||||||
let summary = chalk.bold.blue('AI Usage Summary:') + '\n';
|
let summary = chalk.bold.blue('AI Usage Summary:') + '\n';
|
||||||
@@ -2320,7 +2321,10 @@ function displayAiUsageSummary(telemetryData, outputType = 'cli') {
|
|||||||
summary += chalk.gray(
|
summary += chalk.gray(
|
||||||
` Tokens: ${totalTokens} (Input: ${inputTokens}, Output: ${outputTokens})\n`
|
` Tokens: ${totalTokens} (Input: ${inputTokens}, Output: ${outputTokens})\n`
|
||||||
);
|
);
|
||||||
summary += chalk.gray(` Est. Cost: $${totalCost.toFixed(6)}`);
|
|
||||||
|
// Show "Unknown" if pricing data is not available, otherwise show the cost
|
||||||
|
const costDisplay = isUnknownCost ? 'Unknown' : `$${totalCost.toFixed(6)}`;
|
||||||
|
summary += chalk.gray(` Est. Cost: ${costDisplay}`);
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
boxen(summary, {
|
boxen(summary, {
|
||||||
|
|||||||
@@ -28,6 +28,13 @@ export class BaseAIProvider {
|
|||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
*/
|
*/
|
||||||
this.needsExplicitJsonSchema = false;
|
this.needsExplicitJsonSchema = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this provider supports temperature parameter
|
||||||
|
* Can be overridden by subclasses
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
this.supportsTemperature = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -168,7 +175,9 @@ export class BaseAIProvider {
|
|||||||
model: client(params.modelId),
|
model: client(params.modelId),
|
||||||
messages: params.messages,
|
messages: params.messages,
|
||||||
...this.prepareTokenParam(params.modelId, params.maxTokens),
|
...this.prepareTokenParam(params.modelId, params.maxTokens),
|
||||||
temperature: params.temperature
|
...(this.supportsTemperature && params.temperature !== undefined
|
||||||
|
? { temperature: params.temperature }
|
||||||
|
: {})
|
||||||
});
|
});
|
||||||
|
|
||||||
log(
|
log(
|
||||||
@@ -176,12 +185,19 @@ export class BaseAIProvider {
|
|||||||
`${this.name} generateText completed successfully for model: ${params.modelId}`
|
`${this.name} generateText completed successfully for model: ${params.modelId}`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const inputTokens =
|
||||||
|
result.usage?.inputTokens ?? result.usage?.promptTokens ?? 0;
|
||||||
|
const outputTokens =
|
||||||
|
result.usage?.outputTokens ?? result.usage?.completionTokens ?? 0;
|
||||||
|
const totalTokens =
|
||||||
|
result.usage?.totalTokens ?? inputTokens + outputTokens;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
text: result.text,
|
text: result.text,
|
||||||
usage: {
|
usage: {
|
||||||
inputTokens: result.usage?.promptTokens,
|
inputTokens,
|
||||||
outputTokens: result.usage?.completionTokens,
|
outputTokens,
|
||||||
totalTokens: result.usage?.totalTokens
|
totalTokens
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -204,7 +220,9 @@ export class BaseAIProvider {
|
|||||||
model: client(params.modelId),
|
model: client(params.modelId),
|
||||||
messages: params.messages,
|
messages: params.messages,
|
||||||
...this.prepareTokenParam(params.modelId, params.maxTokens),
|
...this.prepareTokenParam(params.modelId, params.maxTokens),
|
||||||
temperature: params.temperature
|
...(this.supportsTemperature && params.temperature !== undefined
|
||||||
|
? { temperature: params.temperature }
|
||||||
|
: {})
|
||||||
});
|
});
|
||||||
|
|
||||||
log(
|
log(
|
||||||
@@ -242,7 +260,9 @@ export class BaseAIProvider {
|
|||||||
schema: zodSchema(params.schema),
|
schema: zodSchema(params.schema),
|
||||||
mode: params.mode || 'auto',
|
mode: params.mode || 'auto',
|
||||||
maxOutputTokens: params.maxTokens,
|
maxOutputTokens: params.maxTokens,
|
||||||
temperature: params.temperature
|
...(this.supportsTemperature && params.temperature !== undefined
|
||||||
|
? { temperature: params.temperature }
|
||||||
|
: {})
|
||||||
});
|
});
|
||||||
|
|
||||||
log(
|
log(
|
||||||
@@ -288,7 +308,9 @@ export class BaseAIProvider {
|
|||||||
schemaName: params.objectName,
|
schemaName: params.objectName,
|
||||||
schemaDescription: `Generate a valid JSON object for ${params.objectName}`,
|
schemaDescription: `Generate a valid JSON object for ${params.objectName}`,
|
||||||
maxTokens: params.maxTokens,
|
maxTokens: params.maxTokens,
|
||||||
temperature: params.temperature
|
...(this.supportsTemperature && params.temperature !== undefined
|
||||||
|
? { temperature: params.temperature }
|
||||||
|
: {})
|
||||||
});
|
});
|
||||||
|
|
||||||
log(
|
log(
|
||||||
@@ -296,12 +318,19 @@ export class BaseAIProvider {
|
|||||||
`${this.name} generateObject completed successfully for model: ${params.modelId}`
|
`${this.name} generateObject completed successfully for model: ${params.modelId}`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const inputTokens =
|
||||||
|
result.usage?.inputTokens ?? result.usage?.promptTokens ?? 0;
|
||||||
|
const outputTokens =
|
||||||
|
result.usage?.outputTokens ?? result.usage?.completionTokens ?? 0;
|
||||||
|
const totalTokens =
|
||||||
|
result.usage?.totalTokens ?? inputTokens + outputTokens;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
object: result.object,
|
object: result.object,
|
||||||
usage: {
|
usage: {
|
||||||
inputTokens: result.usage?.promptTokens,
|
inputTokens,
|
||||||
outputTokens: result.usage?.completionTokens,
|
outputTokens,
|
||||||
totalTokens: result.usage?.totalTokens
|
totalTokens
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ export class ClaudeCodeProvider extends BaseAIProvider {
|
|||||||
this.supportedModels = ['sonnet', 'opus'];
|
this.supportedModels = ['sonnet', 'opus'];
|
||||||
// Claude Code requires explicit JSON schema mode
|
// Claude Code requires explicit JSON schema mode
|
||||||
this.needsExplicitJsonSchema = true;
|
this.needsExplicitJsonSchema = true;
|
||||||
|
// Claude Code does not support temperature parameter
|
||||||
|
this.supportsTemperature = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
106
src/ai-providers/codex-cli.js
Normal file
106
src/ai-providers/codex-cli.js
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
/**
|
||||||
|
* src/ai-providers/codex-cli.js
|
||||||
|
*
|
||||||
|
* Codex CLI provider implementation using the ai-sdk-provider-codex-cli package.
|
||||||
|
* This provider uses the local OpenAI Codex CLI with OAuth (preferred) or
|
||||||
|
* an optional OPENAI_CODEX_API_KEY if provided.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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';
|
||||||
|
|
||||||
|
export class CodexCliProvider extends BaseAIProvider {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.name = 'Codex CLI';
|
||||||
|
// Codex CLI has native schema support, no explicit JSON schema mode required
|
||||||
|
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'];
|
||||||
|
// CLI availability check cache
|
||||||
|
this._codexCliChecked = false;
|
||||||
|
this._codexCliAvailable = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Codex CLI does not require an API key when using OAuth via `codex login`.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
isRequiredApiKey() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the environment variable name used when an API key is provided.
|
||||||
|
* Even though the API key is optional for Codex CLI (OAuth-first),
|
||||||
|
* downstream resolution expects a non-throwing implementation.
|
||||||
|
* Uses OPENAI_CODEX_API_KEY to avoid conflicts with OpenAI provider.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
getRequiredApiKeyName() {
|
||||||
|
return 'OPENAI_CODEX_API_KEY';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional CLI availability check; provide helpful guidance if missing.
|
||||||
|
*/
|
||||||
|
validateAuth() {
|
||||||
|
if (process.env.NODE_ENV === 'test') return;
|
||||||
|
|
||||||
|
if (!this._codexCliChecked) {
|
||||||
|
try {
|
||||||
|
execSync('codex --version', { stdio: 'pipe', timeout: 1000 });
|
||||||
|
this._codexCliAvailable = true;
|
||||||
|
} catch (error) {
|
||||||
|
this._codexCliAvailable = false;
|
||||||
|
log(
|
||||||
|
'warn',
|
||||||
|
'Codex CLI not detected. Install with: npm i -g @openai/codex or enable fallback with allowNpx.'
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
this._codexCliChecked = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a Codex CLI client instance
|
||||||
|
* @param {object} params
|
||||||
|
* @param {string} [params.commandName] - Command name for settings lookup
|
||||||
|
* @param {string} [params.apiKey] - Optional API key (injected as OPENAI_API_KEY for Codex CLI)
|
||||||
|
* @returns {Function}
|
||||||
|
*/
|
||||||
|
getClient(params = {}) {
|
||||||
|
try {
|
||||||
|
// Merge global + command-specific settings from config
|
||||||
|
const settings = getCodexCliSettingsForCommand(params.commandName) || {};
|
||||||
|
|
||||||
|
// Inject API key only if explicitly provided; OAuth is the primary path
|
||||||
|
const defaultSettings = {
|
||||||
|
...settings,
|
||||||
|
...(params.apiKey
|
||||||
|
? { env: { ...(settings.env || {}), OPENAI_API_KEY: params.apiKey } }
|
||||||
|
: {})
|
||||||
|
};
|
||||||
|
|
||||||
|
return createCodexCli({ defaultSettings });
|
||||||
|
} catch (error) {
|
||||||
|
const msg = String(error?.message || '');
|
||||||
|
const code = error?.code;
|
||||||
|
if (code === 'ENOENT' || /codex/i.test(msg)) {
|
||||||
|
const enhancedError = new Error(
|
||||||
|
`Codex CLI not available. Please install Codex CLI first. Original error: ${error.message}`
|
||||||
|
);
|
||||||
|
enhancedError.cause = error;
|
||||||
|
this.handleError('Codex CLI initialization', enhancedError);
|
||||||
|
} else {
|
||||||
|
this.handleError('client initialization', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,8 @@ export class GeminiCliProvider extends BaseAIProvider {
|
|||||||
this.name = 'Gemini CLI';
|
this.name = 'Gemini CLI';
|
||||||
// Gemini CLI requires explicit JSON schema mode
|
// Gemini CLI requires explicit JSON schema mode
|
||||||
this.needsExplicitJsonSchema = true;
|
this.needsExplicitJsonSchema = true;
|
||||||
|
// Gemini CLI does not support temperature parameter
|
||||||
|
this.supportsTemperature = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ export class GrokCliProvider extends BaseAIProvider {
|
|||||||
this.name = 'Grok CLI';
|
this.name = 'Grok CLI';
|
||||||
// Grok CLI requires explicit JSON schema mode
|
// Grok CLI requires explicit JSON schema mode
|
||||||
this.needsExplicitJsonSchema = true;
|
this.needsExplicitJsonSchema = true;
|
||||||
|
// Grok CLI does not support temperature parameter
|
||||||
|
this.supportsTemperature = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -17,3 +17,4 @@ export { VertexAIProvider } from './google-vertex.js';
|
|||||||
export { ClaudeCodeProvider } from './claude-code.js';
|
export { ClaudeCodeProvider } from './claude-code.js';
|
||||||
export { GeminiCliProvider } from './gemini-cli.js';
|
export { GeminiCliProvider } from './gemini-cli.js';
|
||||||
export { GrokCliProvider } from './grok-cli.js';
|
export { GrokCliProvider } from './grok-cli.js';
|
||||||
|
export { CodexCliProvider } from './codex-cli.js';
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ export const CUSTOM_PROVIDERS = {
|
|||||||
CLAUDE_CODE: 'claude-code',
|
CLAUDE_CODE: 'claude-code',
|
||||||
MCP: 'mcp',
|
MCP: 'mcp',
|
||||||
GEMINI_CLI: 'gemini-cli',
|
GEMINI_CLI: 'gemini-cli',
|
||||||
GROK_CLI: 'grok-cli'
|
GROK_CLI: 'grok-cli',
|
||||||
|
CODEX_CLI: 'codex-cli'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Custom providers array (for backward compatibility and iteration)
|
// Custom providers array (for backward compatibility and iteration)
|
||||||
|
|||||||
62
tests/integration/providers/temperature-support.test.js
Normal file
62
tests/integration/providers/temperature-support.test.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
/**
|
||||||
|
* Integration Tests for Provider Temperature Support
|
||||||
|
*
|
||||||
|
* This test suite verifies that all providers correctly declare their
|
||||||
|
* temperature support capabilities. CLI providers should have
|
||||||
|
* supportsTemperature = false, while standard API providers should
|
||||||
|
* have supportsTemperature = true.
|
||||||
|
*
|
||||||
|
* These tests are separated from unit tests to avoid coupling
|
||||||
|
* base provider tests with concrete provider implementations.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ClaudeCodeProvider } from '../../../src/ai-providers/claude-code.js';
|
||||||
|
import { CodexCliProvider } from '../../../src/ai-providers/codex-cli.js';
|
||||||
|
import { GeminiCliProvider } from '../../../src/ai-providers/gemini-cli.js';
|
||||||
|
import { GrokCliProvider } from '../../../src/ai-providers/grok-cli.js';
|
||||||
|
import { AnthropicAIProvider } from '../../../src/ai-providers/anthropic.js';
|
||||||
|
import { OpenAIProvider } from '../../../src/ai-providers/openai.js';
|
||||||
|
import { GoogleAIProvider } from '../../../src/ai-providers/google.js';
|
||||||
|
import { PerplexityAIProvider } from '../../../src/ai-providers/perplexity.js';
|
||||||
|
import { XAIProvider } from '../../../src/ai-providers/xai.js';
|
||||||
|
import { GroqProvider } from '../../../src/ai-providers/groq.js';
|
||||||
|
import { OpenRouterAIProvider } from '../../../src/ai-providers/openrouter.js';
|
||||||
|
import { OllamaAIProvider } from '../../../src/ai-providers/ollama.js';
|
||||||
|
import { BedrockAIProvider } from '../../../src/ai-providers/bedrock.js';
|
||||||
|
import { AzureProvider } from '../../../src/ai-providers/azure.js';
|
||||||
|
import { VertexAIProvider } from '../../../src/ai-providers/google-vertex.js';
|
||||||
|
|
||||||
|
describe('Provider Temperature Support', () => {
|
||||||
|
describe('CLI Providers', () => {
|
||||||
|
it('should verify CLI providers have supportsTemperature = false', () => {
|
||||||
|
expect(new ClaudeCodeProvider().supportsTemperature).toBe(false);
|
||||||
|
expect(new CodexCliProvider().supportsTemperature).toBe(false);
|
||||||
|
expect(new GeminiCliProvider().supportsTemperature).toBe(false);
|
||||||
|
expect(new GrokCliProvider().supportsTemperature).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Standard API Providers', () => {
|
||||||
|
it('should verify standard providers have supportsTemperature = true', () => {
|
||||||
|
expect(new AnthropicAIProvider().supportsTemperature).toBe(true);
|
||||||
|
expect(new OpenAIProvider().supportsTemperature).toBe(true);
|
||||||
|
expect(new GoogleAIProvider().supportsTemperature).toBe(true);
|
||||||
|
expect(new PerplexityAIProvider().supportsTemperature).toBe(true);
|
||||||
|
expect(new XAIProvider().supportsTemperature).toBe(true);
|
||||||
|
expect(new GroqProvider().supportsTemperature).toBe(true);
|
||||||
|
expect(new OpenRouterAIProvider().supportsTemperature).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Special Case Providers', () => {
|
||||||
|
it('should verify Ollama provider has supportsTemperature = true', () => {
|
||||||
|
expect(new OllamaAIProvider().supportsTemperature).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should verify cloud providers have supportsTemperature = true', () => {
|
||||||
|
expect(new BedrockAIProvider().supportsTemperature).toBe(true);
|
||||||
|
expect(new AzureProvider().supportsTemperature).toBe(true);
|
||||||
|
expect(new VertexAIProvider().supportsTemperature).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
669
tests/unit/ai-providers/base-provider.test.js
Normal file
669
tests/unit/ai-providers/base-provider.test.js
Normal file
@@ -0,0 +1,669 @@
|
|||||||
|
import { jest } from '@jest/globals';
|
||||||
|
|
||||||
|
// Mock the 'ai' SDK
|
||||||
|
const mockGenerateText = jest.fn();
|
||||||
|
const mockGenerateObject = jest.fn();
|
||||||
|
const mockNoObjectGeneratedError = class NoObjectGeneratedError extends Error {
|
||||||
|
static isInstance(error) {
|
||||||
|
return error instanceof mockNoObjectGeneratedError;
|
||||||
|
}
|
||||||
|
constructor(cause) {
|
||||||
|
super('No object generated');
|
||||||
|
this.cause = cause;
|
||||||
|
this.usage = cause.usage;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const mockJSONParseError = class JSONParseError extends Error {
|
||||||
|
constructor(text) {
|
||||||
|
super('JSON parse error');
|
||||||
|
this.text = text;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.unstable_mockModule('ai', () => ({
|
||||||
|
generateText: mockGenerateText,
|
||||||
|
streamText: jest.fn(),
|
||||||
|
generateObject: mockGenerateObject,
|
||||||
|
streamObject: jest.fn(),
|
||||||
|
zodSchema: jest.fn((schema) => schema),
|
||||||
|
NoObjectGeneratedError: mockNoObjectGeneratedError,
|
||||||
|
JSONParseError: mockJSONParseError
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock jsonrepair
|
||||||
|
const mockJsonrepair = jest.fn();
|
||||||
|
jest.unstable_mockModule('jsonrepair', () => ({
|
||||||
|
jsonrepair: mockJsonrepair
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock logging and utilities
|
||||||
|
jest.unstable_mockModule('../../../scripts/modules/utils.js', () => ({
|
||||||
|
log: jest.fn(),
|
||||||
|
findProjectRoot: jest.fn(() => '/mock/project/root'),
|
||||||
|
isEmpty: jest.fn(
|
||||||
|
(val) =>
|
||||||
|
!val ||
|
||||||
|
(Array.isArray(val) && val.length === 0) ||
|
||||||
|
(typeof val === 'object' && Object.keys(val).length === 0)
|
||||||
|
),
|
||||||
|
resolveEnvVariable: jest.fn((key) => process.env[key])
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Import after mocking
|
||||||
|
const { BaseAIProvider } = await import(
|
||||||
|
'../../../src/ai-providers/base-provider.js'
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('BaseAIProvider', () => {
|
||||||
|
let testProvider;
|
||||||
|
let mockClient;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Create a concrete test provider
|
||||||
|
class TestProvider extends BaseAIProvider {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.name = 'TestProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
getRequiredApiKeyName() {
|
||||||
|
return 'TEST_API_KEY';
|
||||||
|
}
|
||||||
|
|
||||||
|
async getClient() {
|
||||||
|
return mockClient;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mockClient = jest.fn((modelId) => ({ modelId }));
|
||||||
|
jest.clearAllMocks();
|
||||||
|
testProvider = new TestProvider();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('1. Parameter Validation - Catches Invalid Inputs', () => {
|
||||||
|
describe('validateAuth', () => {
|
||||||
|
it('should throw when API key is missing', () => {
|
||||||
|
expect(() => testProvider.validateAuth({})).toThrow(
|
||||||
|
'TestProvider API key is required'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass when API key is provided', () => {
|
||||||
|
expect(() =>
|
||||||
|
testProvider.validateAuth({ apiKey: 'test-key' })
|
||||||
|
).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('validateParams', () => {
|
||||||
|
it('should throw when model ID is missing', () => {
|
||||||
|
expect(() => testProvider.validateParams({ apiKey: 'key' })).toThrow(
|
||||||
|
'TestProvider Model ID is required'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw when both API key and model ID are missing', () => {
|
||||||
|
expect(() => testProvider.validateParams({})).toThrow(
|
||||||
|
'TestProvider API key is required'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('validateOptionalParams', () => {
|
||||||
|
it('should throw for temperature below 0', () => {
|
||||||
|
expect(() =>
|
||||||
|
testProvider.validateOptionalParams({ temperature: -0.1 })
|
||||||
|
).toThrow('Temperature must be between 0 and 1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw for temperature above 1', () => {
|
||||||
|
expect(() =>
|
||||||
|
testProvider.validateOptionalParams({ temperature: 1.1 })
|
||||||
|
).toThrow('Temperature must be between 0 and 1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept temperature at boundaries', () => {
|
||||||
|
expect(() =>
|
||||||
|
testProvider.validateOptionalParams({ temperature: 0 })
|
||||||
|
).not.toThrow();
|
||||||
|
expect(() =>
|
||||||
|
testProvider.validateOptionalParams({ temperature: 1 })
|
||||||
|
).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw for invalid maxTokens values', () => {
|
||||||
|
expect(() =>
|
||||||
|
testProvider.validateOptionalParams({ maxTokens: 0 })
|
||||||
|
).toThrow('maxTokens must be a finite number greater than 0');
|
||||||
|
expect(() =>
|
||||||
|
testProvider.validateOptionalParams({ maxTokens: -100 })
|
||||||
|
).toThrow('maxTokens must be a finite number greater than 0');
|
||||||
|
expect(() =>
|
||||||
|
testProvider.validateOptionalParams({ maxTokens: Infinity })
|
||||||
|
).toThrow('maxTokens must be a finite number greater than 0');
|
||||||
|
expect(() =>
|
||||||
|
testProvider.validateOptionalParams({ maxTokens: 'invalid' })
|
||||||
|
).toThrow('maxTokens must be a finite number greater than 0');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('validateMessages', () => {
|
||||||
|
it('should throw for null/undefined messages', async () => {
|
||||||
|
await expect(
|
||||||
|
testProvider.generateText({
|
||||||
|
apiKey: 'key',
|
||||||
|
modelId: 'model',
|
||||||
|
messages: null
|
||||||
|
})
|
||||||
|
).rejects.toThrow('Invalid or empty messages array provided');
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
testProvider.generateText({
|
||||||
|
apiKey: 'key',
|
||||||
|
modelId: 'model',
|
||||||
|
messages: undefined
|
||||||
|
})
|
||||||
|
).rejects.toThrow('Invalid or empty messages array provided');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw for empty messages array', async () => {
|
||||||
|
await expect(
|
||||||
|
testProvider.generateText({
|
||||||
|
apiKey: 'key',
|
||||||
|
modelId: 'model',
|
||||||
|
messages: []
|
||||||
|
})
|
||||||
|
).rejects.toThrow('Invalid or empty messages array provided');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw for messages without role or content', async () => {
|
||||||
|
await expect(
|
||||||
|
testProvider.generateText({
|
||||||
|
apiKey: 'key',
|
||||||
|
modelId: 'model',
|
||||||
|
messages: [{ content: 'test' }] // missing role
|
||||||
|
})
|
||||||
|
).rejects.toThrow(
|
||||||
|
'Invalid message format. Each message must have role and content'
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
testProvider.generateText({
|
||||||
|
apiKey: 'key',
|
||||||
|
modelId: 'model',
|
||||||
|
messages: [{ role: 'user' }] // missing content
|
||||||
|
})
|
||||||
|
).rejects.toThrow(
|
||||||
|
'Invalid message format. Each message must have role and content'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('2. Error Handling - Proper Error Context', () => {
|
||||||
|
it('should wrap API errors with context', async () => {
|
||||||
|
const apiError = new Error('API rate limit exceeded');
|
||||||
|
mockGenerateText.mockRejectedValue(apiError);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
testProvider.generateText({
|
||||||
|
apiKey: 'key',
|
||||||
|
modelId: 'model',
|
||||||
|
messages: [{ role: 'user', content: 'test' }]
|
||||||
|
})
|
||||||
|
).rejects.toThrow(
|
||||||
|
'TestProvider API error during text generation: API rate limit exceeded'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle errors without message property', async () => {
|
||||||
|
const apiError = { code: 'NETWORK_ERROR' };
|
||||||
|
mockGenerateText.mockRejectedValue(apiError);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
testProvider.generateText({
|
||||||
|
apiKey: 'key',
|
||||||
|
modelId: 'model',
|
||||||
|
messages: [{ role: 'user', content: 'test' }]
|
||||||
|
})
|
||||||
|
).rejects.toThrow(
|
||||||
|
'TestProvider API error during text generation: Unknown error occurred'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('3. Abstract Class Protection', () => {
|
||||||
|
it('should prevent direct instantiation of BaseAIProvider', () => {
|
||||||
|
expect(() => new BaseAIProvider()).toThrow(
|
||||||
|
'BaseAIProvider cannot be instantiated directly'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw when abstract methods are not implemented', () => {
|
||||||
|
class IncompleteProvider extends BaseAIProvider {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const provider = new IncompleteProvider();
|
||||||
|
|
||||||
|
expect(() => provider.getClient()).toThrow(
|
||||||
|
'getClient must be implemented by provider'
|
||||||
|
);
|
||||||
|
expect(() => provider.getRequiredApiKeyName()).toThrow(
|
||||||
|
'getRequiredApiKeyName must be implemented by provider'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('4. Token Parameter Preparation', () => {
|
||||||
|
it('should convert maxTokens to maxOutputTokens as integer', () => {
|
||||||
|
const result = testProvider.prepareTokenParam('model', 1000.7);
|
||||||
|
expect(result).toEqual({ maxOutputTokens: 1000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle string numbers', () => {
|
||||||
|
const result = testProvider.prepareTokenParam('model', '500');
|
||||||
|
expect(result).toEqual({ maxOutputTokens: 500 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty object when maxTokens is undefined', () => {
|
||||||
|
const result = testProvider.prepareTokenParam('model', undefined);
|
||||||
|
expect(result).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should floor decimal values', () => {
|
||||||
|
const result = testProvider.prepareTokenParam('model', 999.99);
|
||||||
|
expect(result).toEqual({ maxOutputTokens: 999 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('5. JSON Repair for Malformed Responses', () => {
|
||||||
|
it('should repair malformed JSON in generateObject errors', async () => {
|
||||||
|
const malformedJson = '{"key": "value",,}'; // Double comma
|
||||||
|
const repairedJson = '{"key": "value"}';
|
||||||
|
|
||||||
|
const parseError = new mockJSONParseError(malformedJson);
|
||||||
|
const noObjectError = new mockNoObjectGeneratedError(parseError);
|
||||||
|
noObjectError.usage = {
|
||||||
|
promptTokens: 100,
|
||||||
|
completionTokens: 50,
|
||||||
|
totalTokens: 150
|
||||||
|
};
|
||||||
|
|
||||||
|
mockGenerateObject.mockRejectedValue(noObjectError);
|
||||||
|
mockJsonrepair.mockReturnValue(repairedJson);
|
||||||
|
|
||||||
|
const result = await testProvider.generateObject({
|
||||||
|
apiKey: 'key',
|
||||||
|
modelId: 'model',
|
||||||
|
messages: [{ role: 'user', content: 'test' }],
|
||||||
|
schema: { type: 'object' },
|
||||||
|
objectName: 'TestObject'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockJsonrepair).toHaveBeenCalledWith(malformedJson);
|
||||||
|
expect(result).toEqual({
|
||||||
|
object: { key: 'value' },
|
||||||
|
usage: {
|
||||||
|
inputTokens: 100,
|
||||||
|
outputTokens: 50,
|
||||||
|
totalTokens: 150
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw original error when JSON repair fails', async () => {
|
||||||
|
const malformedJson = 'not even close to JSON';
|
||||||
|
const parseError = new mockJSONParseError(malformedJson);
|
||||||
|
const noObjectError = new mockNoObjectGeneratedError(parseError);
|
||||||
|
|
||||||
|
mockGenerateObject.mockRejectedValue(noObjectError);
|
||||||
|
mockJsonrepair.mockImplementation(() => {
|
||||||
|
throw new Error('Cannot repair this JSON');
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
testProvider.generateObject({
|
||||||
|
apiKey: 'key',
|
||||||
|
modelId: 'model',
|
||||||
|
messages: [{ role: 'user', content: 'test' }],
|
||||||
|
schema: { type: 'object' },
|
||||||
|
objectName: 'TestObject'
|
||||||
|
})
|
||||||
|
).rejects.toThrow('TestProvider API error during object generation');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle non-JSON parse errors normally', async () => {
|
||||||
|
const regularError = new Error('Network timeout');
|
||||||
|
mockGenerateObject.mockRejectedValue(regularError);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
testProvider.generateObject({
|
||||||
|
apiKey: 'key',
|
||||||
|
modelId: 'model',
|
||||||
|
messages: [{ role: 'user', content: 'test' }],
|
||||||
|
schema: { type: 'object' },
|
||||||
|
objectName: 'TestObject'
|
||||||
|
})
|
||||||
|
).rejects.toThrow(
|
||||||
|
'TestProvider API error during object generation: Network timeout'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockJsonrepair).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('6. Usage Token Normalization', () => {
|
||||||
|
it('should normalize different token formats in generateText', async () => {
|
||||||
|
// Test promptTokens/completionTokens format (older format)
|
||||||
|
mockGenerateText.mockResolvedValue({
|
||||||
|
text: 'response',
|
||||||
|
usage: { promptTokens: 10, completionTokens: 5 }
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = await testProvider.generateText({
|
||||||
|
apiKey: 'key',
|
||||||
|
modelId: 'model',
|
||||||
|
messages: [{ role: 'user', content: 'test' }]
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.usage).toEqual({
|
||||||
|
inputTokens: 10,
|
||||||
|
outputTokens: 5,
|
||||||
|
totalTokens: 15
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test inputTokens/outputTokens format (newer format)
|
||||||
|
mockGenerateText.mockResolvedValue({
|
||||||
|
text: 'response',
|
||||||
|
usage: { inputTokens: 20, outputTokens: 10, totalTokens: 30 }
|
||||||
|
});
|
||||||
|
|
||||||
|
result = await testProvider.generateText({
|
||||||
|
apiKey: 'key',
|
||||||
|
modelId: 'model',
|
||||||
|
messages: [{ role: 'user', content: 'test' }]
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.usage).toEqual({
|
||||||
|
inputTokens: 20,
|
||||||
|
outputTokens: 10,
|
||||||
|
totalTokens: 30
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle missing usage data gracefully', async () => {
|
||||||
|
mockGenerateText.mockResolvedValue({
|
||||||
|
text: 'response',
|
||||||
|
usage: undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await testProvider.generateText({
|
||||||
|
apiKey: 'key',
|
||||||
|
modelId: 'model',
|
||||||
|
messages: [{ role: 'user', content: 'test' }]
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.usage).toEqual({
|
||||||
|
inputTokens: 0,
|
||||||
|
outputTokens: 0,
|
||||||
|
totalTokens: 0
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate totalTokens when missing', async () => {
|
||||||
|
mockGenerateText.mockResolvedValue({
|
||||||
|
text: 'response',
|
||||||
|
usage: { inputTokens: 15, outputTokens: 25 }
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await testProvider.generateText({
|
||||||
|
apiKey: 'key',
|
||||||
|
modelId: 'model',
|
||||||
|
messages: [{ role: 'user', content: 'test' }]
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.usage.totalTokens).toBe(40);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('7. Schema Validation for Object Methods', () => {
|
||||||
|
it('should throw when schema is missing for generateObject', async () => {
|
||||||
|
await expect(
|
||||||
|
testProvider.generateObject({
|
||||||
|
apiKey: 'key',
|
||||||
|
modelId: 'model',
|
||||||
|
messages: [{ role: 'user', content: 'test' }],
|
||||||
|
objectName: 'TestObject'
|
||||||
|
// missing schema
|
||||||
|
})
|
||||||
|
).rejects.toThrow('Schema is required for object generation');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw when objectName is missing for generateObject', async () => {
|
||||||
|
await expect(
|
||||||
|
testProvider.generateObject({
|
||||||
|
apiKey: 'key',
|
||||||
|
modelId: 'model',
|
||||||
|
messages: [{ role: 'user', content: 'test' }],
|
||||||
|
schema: { type: 'object' }
|
||||||
|
// missing objectName
|
||||||
|
})
|
||||||
|
).rejects.toThrow('Object name is required for object generation');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw when schema is missing for streamObject', async () => {
|
||||||
|
await expect(
|
||||||
|
testProvider.streamObject({
|
||||||
|
apiKey: 'key',
|
||||||
|
modelId: 'model',
|
||||||
|
messages: [{ role: 'user', content: 'test' }]
|
||||||
|
// missing schema
|
||||||
|
})
|
||||||
|
).rejects.toThrow('Schema is required for object streaming');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use json mode when needsExplicitJsonSchema is true', async () => {
|
||||||
|
testProvider.needsExplicitJsonSchema = true;
|
||||||
|
mockGenerateObject.mockResolvedValue({
|
||||||
|
object: { test: 'value' },
|
||||||
|
usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 }
|
||||||
|
});
|
||||||
|
|
||||||
|
await testProvider.generateObject({
|
||||||
|
apiKey: 'key',
|
||||||
|
modelId: 'model',
|
||||||
|
messages: [{ role: 'user', content: 'test' }],
|
||||||
|
schema: { type: 'object' },
|
||||||
|
objectName: 'TestObject'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockGenerateObject).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
mode: 'json' // Should be 'json' not 'auto'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('8. Integration Points - Client Creation', () => {
|
||||||
|
it('should pass params to getClient method', async () => {
|
||||||
|
const getClientSpy = jest.spyOn(testProvider, 'getClient');
|
||||||
|
mockGenerateText.mockResolvedValue({
|
||||||
|
text: 'response',
|
||||||
|
usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 }
|
||||||
|
});
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
apiKey: 'test-key',
|
||||||
|
modelId: 'test-model',
|
||||||
|
messages: [{ role: 'user', content: 'test' }],
|
||||||
|
customParam: 'custom-value'
|
||||||
|
};
|
||||||
|
|
||||||
|
await testProvider.generateText(params);
|
||||||
|
|
||||||
|
expect(getClientSpy).toHaveBeenCalledWith(params);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use client with correct model ID', async () => {
|
||||||
|
mockGenerateText.mockResolvedValue({
|
||||||
|
text: 'response',
|
||||||
|
usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 }
|
||||||
|
});
|
||||||
|
|
||||||
|
await testProvider.generateText({
|
||||||
|
apiKey: 'key',
|
||||||
|
modelId: 'gpt-4-turbo',
|
||||||
|
messages: [{ role: 'user', content: 'test' }]
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockClient).toHaveBeenCalledWith('gpt-4-turbo');
|
||||||
|
expect(mockGenerateText).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
model: { modelId: 'gpt-4-turbo' }
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('9. Edge Cases - Boundary Conditions', () => {
|
||||||
|
it('should handle zero maxTokens gracefully', () => {
|
||||||
|
// This should throw in validation
|
||||||
|
expect(() =>
|
||||||
|
testProvider.validateOptionalParams({ maxTokens: 0 })
|
||||||
|
).toThrow('maxTokens must be a finite number greater than 0');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle very large maxTokens', () => {
|
||||||
|
const result = testProvider.prepareTokenParam('model', 999999999);
|
||||||
|
expect(result).toEqual({ maxOutputTokens: 999999999 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle NaN temperature gracefully', () => {
|
||||||
|
// NaN fails the range check (NaN < 0 is false, NaN > 1 is also false)
|
||||||
|
// But NaN is not between 0 and 1, so we need to check the actual behavior
|
||||||
|
// The current implementation doesn't explicitly check for NaN,
|
||||||
|
// it passes because NaN < 0 and NaN > 1 are both false
|
||||||
|
expect(() =>
|
||||||
|
testProvider.validateOptionalParams({ temperature: NaN })
|
||||||
|
).not.toThrow();
|
||||||
|
// This is actually a bug - NaN should be rejected
|
||||||
|
// But we're testing current behavior, not desired behavior
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle concurrent calls safely', async () => {
|
||||||
|
mockGenerateText.mockImplementation(async () => ({
|
||||||
|
text: 'response',
|
||||||
|
usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 }
|
||||||
|
}));
|
||||||
|
|
||||||
|
const promises = Array.from({ length: 10 }, (_, i) =>
|
||||||
|
testProvider.generateText({
|
||||||
|
apiKey: 'key',
|
||||||
|
modelId: `model-${i}`,
|
||||||
|
messages: [{ role: 'user', content: `test-${i}` }]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const results = await Promise.all(promises);
|
||||||
|
expect(results).toHaveLength(10);
|
||||||
|
expect(mockClient).toHaveBeenCalledTimes(10);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('10. Default Behavior - isRequiredApiKey', () => {
|
||||||
|
it('should return true by default for isRequiredApiKey', () => {
|
||||||
|
expect(testProvider.isRequiredApiKey()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow override of isRequiredApiKey', () => {
|
||||||
|
class NoAuthProvider extends BaseAIProvider {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
isRequiredApiKey() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
validateAuth() {
|
||||||
|
// Override to not require API key
|
||||||
|
}
|
||||||
|
getClient() {
|
||||||
|
return mockClient;
|
||||||
|
}
|
||||||
|
getRequiredApiKeyName() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const provider = new NoAuthProvider();
|
||||||
|
expect(provider.isRequiredApiKey()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('11. Temperature Filtering - CLI vs Standard Providers', () => {
|
||||||
|
const mockStreamText = jest.fn();
|
||||||
|
const mockStreamObject = jest.fn();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockStreamText.mockReset();
|
||||||
|
mockStreamObject.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include temperature in generateText when supported', async () => {
|
||||||
|
testProvider.supportsTemperature = true;
|
||||||
|
mockGenerateText.mockResolvedValue({
|
||||||
|
text: 'response',
|
||||||
|
usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 }
|
||||||
|
});
|
||||||
|
|
||||||
|
await testProvider.generateText({
|
||||||
|
apiKey: 'key',
|
||||||
|
modelId: 'model',
|
||||||
|
messages: [{ role: 'user', content: 'test' }],
|
||||||
|
temperature: 0.7
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockGenerateText).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ temperature: 0.7 })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should exclude temperature in generateText when not supported', async () => {
|
||||||
|
testProvider.supportsTemperature = false;
|
||||||
|
mockGenerateText.mockResolvedValue({
|
||||||
|
text: 'response',
|
||||||
|
usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 }
|
||||||
|
});
|
||||||
|
|
||||||
|
await testProvider.generateText({
|
||||||
|
apiKey: 'key',
|
||||||
|
modelId: 'model',
|
||||||
|
messages: [{ role: 'user', content: 'test' }],
|
||||||
|
temperature: 0.7
|
||||||
|
});
|
||||||
|
|
||||||
|
const callArgs = mockGenerateText.mock.calls[0][0];
|
||||||
|
expect(callArgs).not.toHaveProperty('temperature');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should exclude temperature when undefined even if supported', async () => {
|
||||||
|
testProvider.supportsTemperature = true;
|
||||||
|
mockGenerateText.mockResolvedValue({
|
||||||
|
text: 'response',
|
||||||
|
usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 }
|
||||||
|
});
|
||||||
|
|
||||||
|
await testProvider.generateText({
|
||||||
|
apiKey: 'key',
|
||||||
|
modelId: 'model',
|
||||||
|
messages: [{ role: 'user', content: 'test' }],
|
||||||
|
temperature: undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
const callArgs = mockGenerateText.mock.calls[0][0];
|
||||||
|
expect(callArgs).not.toHaveProperty('temperature');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
92
tests/unit/ai-providers/codex-cli.test.js
Normal file
92
tests/unit/ai-providers/codex-cli.test.js
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { jest } from '@jest/globals';
|
||||||
|
|
||||||
|
// Mock the ai module
|
||||||
|
jest.unstable_mockModule('ai', () => ({
|
||||||
|
generateObject: jest.fn(),
|
||||||
|
generateText: jest.fn(),
|
||||||
|
streamText: jest.fn()
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock the codex-cli SDK module
|
||||||
|
jest.unstable_mockModule('ai-sdk-provider-codex-cli', () => ({
|
||||||
|
createCodexCli: jest.fn((options) => {
|
||||||
|
const provider = (modelId, settings) => ({ id: modelId, settings });
|
||||||
|
provider.languageModel = jest.fn((id, settings) => ({ id, settings }));
|
||||||
|
provider.chat = provider.languageModel;
|
||||||
|
return provider;
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock config getters
|
||||||
|
jest.unstable_mockModule('../../../scripts/modules/config-manager.js', () => ({
|
||||||
|
getCodexCliSettingsForCommand: jest.fn(() => ({ allowNpx: true })),
|
||||||
|
// Provide commonly imported getters to satisfy other module imports if any
|
||||||
|
getDebugFlag: jest.fn(() => false),
|
||||||
|
getLogLevel: jest.fn(() => 'info')
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock base provider
|
||||||
|
jest.unstable_mockModule('../../../src/ai-providers/base-provider.js', () => ({
|
||||||
|
BaseAIProvider: class {
|
||||||
|
constructor() {
|
||||||
|
this.name = 'Base Provider';
|
||||||
|
}
|
||||||
|
handleError(_ctx, err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
validateParams(params) {
|
||||||
|
if (!params.modelId) throw new Error('Model ID is required');
|
||||||
|
}
|
||||||
|
validateMessages(msgs) {
|
||||||
|
if (!Array.isArray(msgs)) throw new Error('Invalid messages array');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { CodexCliProvider } = await import(
|
||||||
|
'../../../src/ai-providers/codex-cli.js'
|
||||||
|
);
|
||||||
|
const { createCodexCli } = await import('ai-sdk-provider-codex-cli');
|
||||||
|
const { getCodexCliSettingsForCommand } = await import(
|
||||||
|
'../../../scripts/modules/config-manager.js'
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('CodexCliProvider', () => {
|
||||||
|
let provider;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
provider = new CodexCliProvider();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets provider name and supported models', () => {
|
||||||
|
expect(provider.name).toBe('Codex CLI');
|
||||||
|
expect(provider.supportedModels).toEqual(['gpt-5', 'gpt-5-codex']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not require API key', () => {
|
||||||
|
expect(provider.isRequiredApiKey()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates client with merged default settings', async () => {
|
||||||
|
const client = await provider.getClient({ commandName: 'parse-prd' });
|
||||||
|
expect(client).toBeDefined();
|
||||||
|
expect(createCodexCli).toHaveBeenCalledWith({
|
||||||
|
defaultSettings: expect.objectContaining({ allowNpx: true })
|
||||||
|
});
|
||||||
|
expect(getCodexCliSettingsForCommand).toHaveBeenCalledWith('parse-prd');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('injects OPENAI_API_KEY only when apiKey provided', async () => {
|
||||||
|
const client = await provider.getClient({
|
||||||
|
commandName: 'expand',
|
||||||
|
apiKey: 'sk-test'
|
||||||
|
});
|
||||||
|
const call = createCodexCli.mock.calls[0][0];
|
||||||
|
expect(call.defaultSettings.env.OPENAI_API_KEY).toBe('sk-test');
|
||||||
|
// Ensure env is not set when apiKey not provided
|
||||||
|
await provider.getClient({ commandName: 'expand' });
|
||||||
|
const second = createCodexCli.mock.calls[1][0];
|
||||||
|
expect(second.defaultSettings.env).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -122,7 +122,7 @@ jest.unstable_mockModule('../../scripts/modules/config-manager.js', () => ({
|
|||||||
getMcpApiKeyStatus: mockGetMcpApiKeyStatus,
|
getMcpApiKeyStatus: mockGetMcpApiKeyStatus,
|
||||||
|
|
||||||
// Providers without API keys
|
// Providers without API keys
|
||||||
providersWithoutApiKeys: ['ollama', 'bedrock', 'gemini-cli']
|
providersWithoutApiKeys: ['ollama', 'bedrock', 'gemini-cli', 'codex-cli']
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock AI Provider Classes with proper methods
|
// Mock AI Provider Classes with proper methods
|
||||||
@@ -158,6 +158,24 @@ const mockOllamaProvider = {
|
|||||||
isRequiredApiKey: jest.fn(() => false)
|
isRequiredApiKey: jest.fn(() => false)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Codex CLI mock provider instance
|
||||||
|
const mockCodexProvider = {
|
||||||
|
generateText: jest.fn(),
|
||||||
|
streamText: jest.fn(),
|
||||||
|
generateObject: jest.fn(),
|
||||||
|
getRequiredApiKeyName: jest.fn(() => 'OPENAI_API_KEY'),
|
||||||
|
isRequiredApiKey: jest.fn(() => false)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Claude Code mock provider instance
|
||||||
|
const mockClaudeProvider = {
|
||||||
|
generateText: jest.fn(),
|
||||||
|
streamText: jest.fn(),
|
||||||
|
generateObject: jest.fn(),
|
||||||
|
getRequiredApiKeyName: jest.fn(() => 'CLAUDE_CODE_API_KEY'),
|
||||||
|
isRequiredApiKey: jest.fn(() => false)
|
||||||
|
};
|
||||||
|
|
||||||
// Mock the provider classes to return our mock instances
|
// Mock the provider classes to return our mock instances
|
||||||
jest.unstable_mockModule('../../src/ai-providers/index.js', () => ({
|
jest.unstable_mockModule('../../src/ai-providers/index.js', () => ({
|
||||||
AnthropicAIProvider: jest.fn(() => mockAnthropicProvider),
|
AnthropicAIProvider: jest.fn(() => mockAnthropicProvider),
|
||||||
@@ -213,13 +231,7 @@ jest.unstable_mockModule('../../src/ai-providers/index.js', () => ({
|
|||||||
getRequiredApiKeyName: jest.fn(() => null),
|
getRequiredApiKeyName: jest.fn(() => null),
|
||||||
isRequiredApiKey: jest.fn(() => false)
|
isRequiredApiKey: jest.fn(() => false)
|
||||||
})),
|
})),
|
||||||
ClaudeCodeProvider: jest.fn(() => ({
|
ClaudeCodeProvider: jest.fn(() => mockClaudeProvider),
|
||||||
generateText: jest.fn(),
|
|
||||||
streamText: jest.fn(),
|
|
||||||
generateObject: jest.fn(),
|
|
||||||
getRequiredApiKeyName: jest.fn(() => 'CLAUDE_CODE_API_KEY'),
|
|
||||||
isRequiredApiKey: jest.fn(() => false)
|
|
||||||
})),
|
|
||||||
GeminiCliProvider: jest.fn(() => ({
|
GeminiCliProvider: jest.fn(() => ({
|
||||||
generateText: jest.fn(),
|
generateText: jest.fn(),
|
||||||
streamText: jest.fn(),
|
streamText: jest.fn(),
|
||||||
@@ -227,6 +239,7 @@ jest.unstable_mockModule('../../src/ai-providers/index.js', () => ({
|
|||||||
getRequiredApiKeyName: jest.fn(() => 'GEMINI_API_KEY'),
|
getRequiredApiKeyName: jest.fn(() => 'GEMINI_API_KEY'),
|
||||||
isRequiredApiKey: jest.fn(() => false)
|
isRequiredApiKey: jest.fn(() => false)
|
||||||
})),
|
})),
|
||||||
|
CodexCliProvider: jest.fn(() => mockCodexProvider),
|
||||||
GrokCliProvider: jest.fn(() => ({
|
GrokCliProvider: jest.fn(() => ({
|
||||||
generateText: jest.fn(),
|
generateText: jest.fn(),
|
||||||
streamText: jest.fn(),
|
streamText: jest.fn(),
|
||||||
@@ -809,5 +822,112 @@ describe('Unified AI Services', () => {
|
|||||||
// Should have gotten the anthropic response
|
// Should have gotten the anthropic response
|
||||||
expect(result.mainResult).toBe('Anthropic response with session key');
|
expect(result.mainResult).toBe('Anthropic response with session key');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- Codex CLI specific tests ---
|
||||||
|
test('should use codex-cli provider without API key (OAuth)', async () => {
|
||||||
|
// Arrange codex-cli as main provider
|
||||||
|
mockGetMainProvider.mockReturnValue('codex-cli');
|
||||||
|
mockGetMainModelId.mockReturnValue('gpt-5-codex');
|
||||||
|
mockGetParametersForRole.mockReturnValue({
|
||||||
|
maxTokens: 128000,
|
||||||
|
temperature: 1
|
||||||
|
});
|
||||||
|
mockGetResponseLanguage.mockReturnValue('English');
|
||||||
|
// No API key in env
|
||||||
|
mockResolveEnvVariable.mockReturnValue(null);
|
||||||
|
// Mock codex generateText response
|
||||||
|
mockCodexProvider.generateText.mockResolvedValueOnce({
|
||||||
|
text: 'ok',
|
||||||
|
usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 }
|
||||||
|
});
|
||||||
|
|
||||||
|
const { generateTextService } = await import(
|
||||||
|
'../../scripts/modules/ai-services-unified.js'
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await generateTextService({
|
||||||
|
role: 'main',
|
||||||
|
prompt: 'Hello Codex',
|
||||||
|
projectRoot: fakeProjectRoot
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.mainResult).toBe('ok');
|
||||||
|
expect(mockCodexProvider.generateText).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
modelId: 'gpt-5-codex',
|
||||||
|
apiKey: null,
|
||||||
|
maxTokens: 128000
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should pass apiKey to codex-cli when provided', async () => {
|
||||||
|
// Arrange codex-cli as main provider
|
||||||
|
mockGetMainProvider.mockReturnValue('codex-cli');
|
||||||
|
mockGetMainModelId.mockReturnValue('gpt-5-codex');
|
||||||
|
mockGetParametersForRole.mockReturnValue({
|
||||||
|
maxTokens: 128000,
|
||||||
|
temperature: 1
|
||||||
|
});
|
||||||
|
mockGetResponseLanguage.mockReturnValue('English');
|
||||||
|
// Provide API key via env resolver
|
||||||
|
mockResolveEnvVariable.mockReturnValue('sk-test');
|
||||||
|
// Mock codex generateText response
|
||||||
|
mockCodexProvider.generateText.mockResolvedValueOnce({
|
||||||
|
text: 'ok-with-key',
|
||||||
|
usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 }
|
||||||
|
});
|
||||||
|
|
||||||
|
const { generateTextService } = await import(
|
||||||
|
'../../scripts/modules/ai-services-unified.js'
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await generateTextService({
|
||||||
|
role: 'main',
|
||||||
|
prompt: 'Hello Codex',
|
||||||
|
projectRoot: fakeProjectRoot
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.mainResult).toBe('ok-with-key');
|
||||||
|
expect(mockCodexProvider.generateText).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
modelId: 'gpt-5-codex',
|
||||||
|
apiKey: 'sk-test'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Claude Code specific test ---
|
||||||
|
test('should pass temperature to claude-code provider (provider handles filtering)', async () => {
|
||||||
|
mockGetMainProvider.mockReturnValue('claude-code');
|
||||||
|
mockGetMainModelId.mockReturnValue('sonnet');
|
||||||
|
mockGetParametersForRole.mockReturnValue({
|
||||||
|
maxTokens: 64000,
|
||||||
|
temperature: 0.7
|
||||||
|
});
|
||||||
|
mockGetResponseLanguage.mockReturnValue('English');
|
||||||
|
mockResolveEnvVariable.mockReturnValue(null);
|
||||||
|
|
||||||
|
mockClaudeProvider.generateText.mockResolvedValueOnce({
|
||||||
|
text: 'ok-claude',
|
||||||
|
usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 }
|
||||||
|
});
|
||||||
|
|
||||||
|
const { generateTextService } = await import(
|
||||||
|
'../../scripts/modules/ai-services-unified.js'
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await generateTextService({
|
||||||
|
role: 'main',
|
||||||
|
prompt: 'Hello Claude',
|
||||||
|
projectRoot: fakeProjectRoot
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.mainResult).toBe('ok-claude');
|
||||||
|
// The provider (BaseAIProvider) is responsible for filtering it based on supportsTemperature
|
||||||
|
const callArgs = mockClaudeProvider.generateText.mock.calls[0][0];
|
||||||
|
expect(callArgs).toHaveProperty('temperature', 0.7);
|
||||||
|
expect(callArgs.maxTokens).toBe(64000);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ const DEFAULT_CONFIG = {
|
|||||||
responseLanguage: 'English'
|
responseLanguage: 'English'
|
||||||
},
|
},
|
||||||
claudeCode: {},
|
claudeCode: {},
|
||||||
|
codexCli: {},
|
||||||
grokCli: {
|
grokCli: {
|
||||||
timeout: 120000,
|
timeout: 120000,
|
||||||
workingDirectory: null,
|
workingDirectory: null,
|
||||||
@@ -642,7 +643,8 @@ describe('getConfig Tests', () => {
|
|||||||
...DEFAULT_CONFIG.claudeCode,
|
...DEFAULT_CONFIG.claudeCode,
|
||||||
...VALID_CUSTOM_CONFIG.claudeCode
|
...VALID_CUSTOM_CONFIG.claudeCode
|
||||||
},
|
},
|
||||||
grokCli: { ...DEFAULT_CONFIG.grokCli }
|
grokCli: { ...DEFAULT_CONFIG.grokCli },
|
||||||
|
codexCli: { ...DEFAULT_CONFIG.codexCli }
|
||||||
};
|
};
|
||||||
expect(config).toEqual(expectedMergedConfig);
|
expect(config).toEqual(expectedMergedConfig);
|
||||||
expect(fsExistsSyncSpy).toHaveBeenCalledWith(MOCK_CONFIG_PATH);
|
expect(fsExistsSyncSpy).toHaveBeenCalledWith(MOCK_CONFIG_PATH);
|
||||||
@@ -685,7 +687,8 @@ describe('getConfig Tests', () => {
|
|||||||
...DEFAULT_CONFIG.claudeCode,
|
...DEFAULT_CONFIG.claudeCode,
|
||||||
...VALID_CUSTOM_CONFIG.claudeCode
|
...VALID_CUSTOM_CONFIG.claudeCode
|
||||||
},
|
},
|
||||||
grokCli: { ...DEFAULT_CONFIG.grokCli }
|
grokCli: { ...DEFAULT_CONFIG.grokCli },
|
||||||
|
codexCli: { ...DEFAULT_CONFIG.codexCli }
|
||||||
};
|
};
|
||||||
expect(config).toEqual(expectedMergedConfig);
|
expect(config).toEqual(expectedMergedConfig);
|
||||||
expect(fsReadFileSyncSpy).toHaveBeenCalledWith(MOCK_CONFIG_PATH, 'utf-8');
|
expect(fsReadFileSyncSpy).toHaveBeenCalledWith(MOCK_CONFIG_PATH, 'utf-8');
|
||||||
@@ -794,7 +797,8 @@ describe('getConfig Tests', () => {
|
|||||||
...DEFAULT_CONFIG.claudeCode,
|
...DEFAULT_CONFIG.claudeCode,
|
||||||
...VALID_CUSTOM_CONFIG.claudeCode
|
...VALID_CUSTOM_CONFIG.claudeCode
|
||||||
},
|
},
|
||||||
grokCli: { ...DEFAULT_CONFIG.grokCli }
|
grokCli: { ...DEFAULT_CONFIG.grokCli },
|
||||||
|
codexCli: { ...DEFAULT_CONFIG.codexCli }
|
||||||
};
|
};
|
||||||
expect(config).toEqual(expectedMergedConfig);
|
expect(config).toEqual(expectedMergedConfig);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -94,7 +94,6 @@ describe('addSubtask function', () => {
|
|||||||
const parentTask = writeCallArgs.tasks.find((t) => t.id === 1);
|
const parentTask = writeCallArgs.tasks.find((t) => t.id === 1);
|
||||||
expect(parentTask.subtasks).toHaveLength(1);
|
expect(parentTask.subtasks).toHaveLength(1);
|
||||||
expect(parentTask.subtasks[0].title).toBe('New Subtask');
|
expect(parentTask.subtasks[0].title).toBe('New Subtask');
|
||||||
expect(mockGenerateTaskFiles).toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should convert an existing task to a subtask', async () => {
|
test('should convert an existing task to a subtask', async () => {
|
||||||
|
|||||||
@@ -88,11 +88,6 @@ describe('moveTask (unit)', () => {
|
|||||||
).rejects.toThrow(/Number of source IDs/);
|
).rejects.toThrow(/Number of source IDs/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('batch move calls generateTaskFiles once when flag true', async () => {
|
|
||||||
await moveTask('tasks.json', '1,2', '3,4', true, { tag: 'master' });
|
|
||||||
expect(generateTaskFiles).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('error when tag invalid', async () => {
|
test('error when tag invalid', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
moveTask('tasks.json', '1', '2', false, { tag: 'ghost' })
|
moveTask('tasks.json', '1', '2', false, { tag: 'ghost' })
|
||||||
|
|||||||
@@ -19,10 +19,10 @@ const getBuildTimeEnvs = () => {
|
|||||||
|
|
||||||
for (const [key, value] of Object.entries(process.env)) {
|
for (const [key, value] of Object.entries(process.env)) {
|
||||||
if (key.startsWith('TM_PUBLIC_')) {
|
if (key.startsWith('TM_PUBLIC_')) {
|
||||||
// Return the actual value, not JSON.stringify'd
|
|
||||||
envs[key] = value || '';
|
envs[key] = value || '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return envs;
|
return envs;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
"build": {
|
"build": {
|
||||||
"dependsOn": ["^build"],
|
"dependsOn": ["^build"],
|
||||||
"outputs": ["dist/**"],
|
"outputs": ["dist/**"],
|
||||||
"outputLogs": "new-only"
|
"outputLogs": "new-only",
|
||||||
|
"env": ["NODE_ENV", "TM_PUBLIC_*"]
|
||||||
},
|
},
|
||||||
"dev": {
|
"dev": {
|
||||||
"cache": false,
|
"cache": false,
|
||||||
|
|||||||
Reference in New Issue
Block a user