Compare commits
8 Commits
docs/auto-
...
task-maste
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0df6595245 | ||
|
|
33e3fbb20f | ||
|
|
5cb7ed557a | ||
|
|
b9e644c556 | ||
|
|
7265a6cf53 | ||
|
|
db6f405f23 | ||
|
|
7b5a7c4495 | ||
|
|
caee040907 |
@@ -1,11 +0,0 @@
|
||||
---
|
||||
"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
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"task-master-ai": patch
|
||||
---
|
||||
|
||||
Improve `analyze-complexity` cli docs and `--research` flag documentation
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
"task-master-ai": minor
|
||||
---
|
||||
|
||||
Add Cursor IDE custom slash command support
|
||||
|
||||
Expose Task Master commands as Cursor slash commands by copying assets/claude/commands to .cursor/commands on profile add and cleaning up on remove.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"task-master-ai": patch
|
||||
---
|
||||
|
||||
Change parent task back to "pending" when all subtasks are in "pending" state
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"task-master-ai": patch
|
||||
---
|
||||
|
||||
Do a quick fix on build
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"task-master-ai": minor
|
||||
---
|
||||
|
||||
Added api keys page on docs website: docs.task-master.dev/getting-started/api-keys
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
"task-master-ai": minor
|
||||
---
|
||||
|
||||
Move to AI SDK v5:
|
||||
|
||||
- Works better with claude-code and gemini-cli as ai providers
|
||||
- Improved openai model family compatibility
|
||||
- Migrate ollama provider to v2
|
||||
- Closes #1223, #1013, #1161, #1174
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
"task-master-ai": minor
|
||||
---
|
||||
|
||||
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.
|
||||
@@ -1,13 +0,0 @@
|
||||
---
|
||||
"task-master-ai": minor
|
||||
---
|
||||
|
||||
Enhanced Roo Code profile with MCP timeout configuration for improved reliability during long-running AI operations. The Roo profile now automatically configures a 300-second timeout for MCP server operations, preventing timeouts during complex tasks like `parse-prd`, `expand-all`, `analyze-complexity`, and `research` operations. This change also replaces static MCP configuration files with programmatic generation for better maintainability.
|
||||
|
||||
**What's New:**
|
||||
- 300-second timeout for MCP operations (up from default 60 seconds)
|
||||
- Programmatic MCP configuration generation (replaces static asset files)
|
||||
- Enhanced reliability for AI-powered operations
|
||||
- Consistent with other AI coding assistant profiles
|
||||
|
||||
**Migration:** No user action required - existing Roo Code installations will automatically receive the enhanced MCP configuration on next initialization.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"task-master-ai": patch
|
||||
---
|
||||
|
||||
Fix Claude Code settings validation for pathToClaudeCodeExecutable
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"task-master-ai": patch
|
||||
---
|
||||
|
||||
Fix sonar deep research model failing, should be called `sonar-deep-research`
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"task-master-ai": minor
|
||||
---
|
||||
|
||||
Upgrade grok-cli ai provider to ai sdk v5
|
||||
@@ -1,8 +0,0 @@
|
||||
---
|
||||
"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)
|
||||
96
CHANGELOG.md
96
CHANGELOG.md
@@ -1,5 +1,101 @@
|
||||
# task-master-ai
|
||||
|
||||
## 0.28.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#1273](https://github.com/eyaltoledano/claude-task-master/pull/1273) [`b43b7ce`](https://github.com/eyaltoledano/claude-task-master/commit/b43b7ce201625eee956fb2f8cd332f238bb78c21) Thanks [@ben-vargas](https://github.com/ben-vargas)! - 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
|
||||
|
||||
- [#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
|
||||
|
||||
- [#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
|
||||
|
||||
- [#1277](https://github.com/eyaltoledano/claude-task-master/pull/1277) [`7b5a7c4`](https://github.com/eyaltoledano/claude-task-master/commit/7b5a7c4495a68b782f7407fc5d0e0d3ae81f42f5) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - 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.
|
||||
|
||||
- [#1276](https://github.com/eyaltoledano/claude-task-master/pull/1276) [`caee040`](https://github.com/eyaltoledano/claude-task-master/commit/caee040907f856d31a660171c9e6d966f23c632e) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fix MCP server error when file parameter not provided - now properly constructs default tasks.json path instead of failing with 'tasksJsonPath is required' error.
|
||||
|
||||
- [#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.28.0-rc.2
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#1273](https://github.com/eyaltoledano/claude-task-master/pull/1273) [`b43b7ce`](https://github.com/eyaltoledano/claude-task-master/commit/b43b7ce201625eee956fb2f8cd332f238bb78c21) Thanks [@ben-vargas](https://github.com/ben-vargas)! - 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
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#1277](https://github.com/eyaltoledano/claude-task-master/pull/1277) [`7b5a7c4`](https://github.com/eyaltoledano/claude-task-master/commit/7b5a7c4495a68b782f7407fc5d0e0d3ae81f42f5) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - 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.
|
||||
|
||||
- [#1276](https://github.com/eyaltoledano/claude-task-master/pull/1276) [`caee040`](https://github.com/eyaltoledano/claude-task-master/commit/caee040907f856d31a660171c9e6d966f23c632e) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fix MCP server error when file parameter not provided - now properly constructs default tasks.json path instead of failing with 'tasksJsonPath is required' error.
|
||||
|
||||
## 0.28.0-rc.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -4,6 +4,13 @@
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tm/core@null
|
||||
|
||||
## null
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tm/core@null
|
||||
|
||||
|
||||
@@ -48,5 +48,6 @@
|
||||
"*": {
|
||||
"*": ["src/*"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": ""
|
||||
}
|
||||
|
||||
255
apps/cli/src/command-registry.ts
Normal file
255
apps/cli/src/command-registry.ts
Normal file
@@ -0,0 +1,255 @@
|
||||
/**
|
||||
* @fileoverview Centralized Command Registry
|
||||
* Provides a single location for registering all CLI commands
|
||||
*/
|
||||
|
||||
import { Command } from 'commander';
|
||||
|
||||
// Import all commands
|
||||
import { ListTasksCommand } from './commands/list.command.js';
|
||||
import { ShowCommand } from './commands/show.command.js';
|
||||
import { AuthCommand } from './commands/auth.command.js';
|
||||
import { ContextCommand } from './commands/context.command.js';
|
||||
import { StartCommand } from './commands/start.command.js';
|
||||
import { SetStatusCommand } from './commands/set-status.command.js';
|
||||
import { ExportCommand } from './commands/export.command.js';
|
||||
|
||||
/**
|
||||
* Command metadata for registration
|
||||
*/
|
||||
export interface CommandMetadata {
|
||||
name: string;
|
||||
description: string;
|
||||
commandClass: typeof Command;
|
||||
category?: 'task' | 'auth' | 'utility' | 'development';
|
||||
}
|
||||
|
||||
/**
|
||||
* Registry of all available commands
|
||||
*/
|
||||
export class CommandRegistry {
|
||||
/**
|
||||
* All available commands with their metadata
|
||||
*/
|
||||
private static commands: CommandMetadata[] = [
|
||||
// Task Management Commands
|
||||
{
|
||||
name: 'list',
|
||||
description: 'List all tasks with filtering and status overview',
|
||||
commandClass: ListTasksCommand as any,
|
||||
category: 'task'
|
||||
},
|
||||
{
|
||||
name: 'show',
|
||||
description: 'Display detailed information about a specific task',
|
||||
commandClass: ShowCommand as any,
|
||||
category: 'task'
|
||||
},
|
||||
{
|
||||
name: 'start',
|
||||
description: 'Start working on a task with claude-code',
|
||||
commandClass: StartCommand as any,
|
||||
category: 'task'
|
||||
},
|
||||
{
|
||||
name: 'set-status',
|
||||
description: 'Update the status of one or more tasks',
|
||||
commandClass: SetStatusCommand as any,
|
||||
category: 'task'
|
||||
},
|
||||
{
|
||||
name: 'export',
|
||||
description: 'Export tasks to external systems',
|
||||
commandClass: ExportCommand as any,
|
||||
category: 'task'
|
||||
},
|
||||
|
||||
// Authentication & Context Commands
|
||||
{
|
||||
name: 'auth',
|
||||
description: 'Manage authentication with tryhamster.com',
|
||||
commandClass: AuthCommand as any,
|
||||
category: 'auth'
|
||||
},
|
||||
{
|
||||
name: 'context',
|
||||
description: 'Manage workspace context (organization/brief)',
|
||||
commandClass: ContextCommand as any,
|
||||
category: 'auth'
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* Register all commands on a program instance
|
||||
* @param program - Commander program to register commands on
|
||||
*/
|
||||
static registerAll(program: Command): void {
|
||||
for (const cmd of this.commands) {
|
||||
this.registerCommand(program, cmd);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register specific commands by category
|
||||
* @param program - Commander program to register commands on
|
||||
* @param category - Category of commands to register
|
||||
*/
|
||||
static registerByCategory(
|
||||
program: Command,
|
||||
category: 'task' | 'auth' | 'utility' | 'development'
|
||||
): void {
|
||||
const categoryCommands = this.commands.filter(
|
||||
(cmd) => cmd.category === category
|
||||
);
|
||||
|
||||
for (const cmd of categoryCommands) {
|
||||
this.registerCommand(program, cmd);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a single command by name
|
||||
* @param program - Commander program to register the command on
|
||||
* @param name - Name of the command to register
|
||||
*/
|
||||
static registerByName(program: Command, name: string): void {
|
||||
const cmd = this.commands.find((c) => c.name === name);
|
||||
if (cmd) {
|
||||
this.registerCommand(program, cmd);
|
||||
} else {
|
||||
throw new Error(`Command '${name}' not found in registry`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a single command
|
||||
* @param program - Commander program to register the command on
|
||||
* @param metadata - Command metadata
|
||||
*/
|
||||
private static registerCommand(
|
||||
program: Command,
|
||||
metadata: CommandMetadata
|
||||
): void {
|
||||
const CommandClass = metadata.commandClass as any;
|
||||
|
||||
// Use the static registration method that all commands have
|
||||
if (CommandClass.registerOn) {
|
||||
CommandClass.registerOn(program);
|
||||
} else if (CommandClass.register) {
|
||||
CommandClass.register(program);
|
||||
} else {
|
||||
// Fallback to creating instance and adding
|
||||
const instance = new CommandClass();
|
||||
program.addCommand(instance);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered command names
|
||||
*/
|
||||
static getCommandNames(): string[] {
|
||||
return this.commands.map((cmd) => cmd.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get commands by category
|
||||
*/
|
||||
static getCommandsByCategory(
|
||||
category: 'task' | 'auth' | 'utility' | 'development'
|
||||
): CommandMetadata[] {
|
||||
return this.commands.filter((cmd) => cmd.category === category);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new command to the registry
|
||||
* @param metadata - Command metadata to add
|
||||
*/
|
||||
static addCommand(metadata: CommandMetadata): void {
|
||||
// Check if command already exists
|
||||
if (this.commands.some((cmd) => cmd.name === metadata.name)) {
|
||||
throw new Error(`Command '${metadata.name}' already exists in registry`);
|
||||
}
|
||||
|
||||
this.commands.push(metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a command from the registry
|
||||
* @param name - Name of the command to remove
|
||||
*/
|
||||
static removeCommand(name: string): boolean {
|
||||
const index = this.commands.findIndex((cmd) => cmd.name === name);
|
||||
if (index >= 0) {
|
||||
this.commands.splice(index, 1);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get command metadata by name
|
||||
* @param name - Name of the command
|
||||
*/
|
||||
static getCommand(name: string): CommandMetadata | undefined {
|
||||
return this.commands.find((cmd) => cmd.name === name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a command exists
|
||||
* @param name - Name of the command
|
||||
*/
|
||||
static hasCommand(name: string): boolean {
|
||||
return this.commands.some((cmd) => cmd.name === name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a formatted list of all commands for display
|
||||
*/
|
||||
static getFormattedCommandList(): string {
|
||||
const categories = {
|
||||
task: 'Task Management',
|
||||
auth: 'Authentication & Context',
|
||||
utility: 'Utilities',
|
||||
development: 'Development'
|
||||
};
|
||||
|
||||
let output = '';
|
||||
|
||||
for (const [category, title] of Object.entries(categories)) {
|
||||
const cmds = this.getCommandsByCategory(
|
||||
category as keyof typeof categories
|
||||
);
|
||||
if (cmds.length > 0) {
|
||||
output += `\n${title}:\n`;
|
||||
for (const cmd of cmds) {
|
||||
output += ` ${cmd.name.padEnd(20)} ${cmd.description}\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to register all CLI commands
|
||||
* @param program - Commander program instance
|
||||
*/
|
||||
export function registerAllCommands(program: Command): void {
|
||||
CommandRegistry.registerAll(program);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to register commands by category
|
||||
* @param program - Commander program instance
|
||||
* @param category - Category to register
|
||||
*/
|
||||
export function registerCommandsByCategory(
|
||||
program: Command,
|
||||
category: 'task' | 'auth' | 'utility' | 'development'
|
||||
): void {
|
||||
CommandRegistry.registerByCategory(program, category);
|
||||
}
|
||||
|
||||
// Export the registry for direct access if needed
|
||||
export default CommandRegistry;
|
||||
@@ -493,18 +493,7 @@ export class AuthCommand extends Command {
|
||||
}
|
||||
|
||||
/**
|
||||
* Static method to register this command on an existing program
|
||||
* This is for gradual migration - allows commands.js to use this
|
||||
*/
|
||||
static registerOn(program: Command): Command {
|
||||
const authCommand = new AuthCommand();
|
||||
program.addCommand(authCommand);
|
||||
return authCommand;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alternative registration that returns the command for chaining
|
||||
* Can also configure the command name if needed
|
||||
* Register this command on an existing program
|
||||
*/
|
||||
static register(program: Command, name?: string): AuthCommand {
|
||||
const authCommand = new AuthCommand(name);
|
||||
|
||||
@@ -694,16 +694,7 @@ export class ContextCommand extends Command {
|
||||
}
|
||||
|
||||
/**
|
||||
* Static method to register this command on an existing program
|
||||
*/
|
||||
static registerOn(program: Command): Command {
|
||||
const contextCommand = new ContextCommand();
|
||||
program.addCommand(contextCommand);
|
||||
return contextCommand;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alternative registration that returns the command for chaining
|
||||
* Register this command on an existing program
|
||||
*/
|
||||
static register(program: Command, name?: string): ContextCommand {
|
||||
const contextCommand = new ContextCommand(name);
|
||||
|
||||
379
apps/cli/src/commands/export.command.ts
Normal file
379
apps/cli/src/commands/export.command.ts
Normal file
@@ -0,0 +1,379 @@
|
||||
/**
|
||||
* @fileoverview Export command for exporting tasks to external systems
|
||||
* Provides functionality to export tasks to Hamster briefs
|
||||
*/
|
||||
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import inquirer from 'inquirer';
|
||||
import ora, { Ora } from 'ora';
|
||||
import {
|
||||
AuthManager,
|
||||
AuthenticationError,
|
||||
type UserContext
|
||||
} from '@tm/core/auth';
|
||||
import { TaskMasterCore, type ExportResult } from '@tm/core';
|
||||
import * as ui from '../utils/ui.js';
|
||||
|
||||
/**
|
||||
* Result type from export command
|
||||
*/
|
||||
export interface ExportCommandResult {
|
||||
success: boolean;
|
||||
action: 'export' | 'validate' | 'cancelled';
|
||||
result?: ExportResult;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* ExportCommand extending Commander's Command class
|
||||
* Handles task export to external systems
|
||||
*/
|
||||
export class ExportCommand extends Command {
|
||||
private authManager: AuthManager;
|
||||
private taskMasterCore?: TaskMasterCore;
|
||||
private lastResult?: ExportCommandResult;
|
||||
|
||||
constructor(name?: string) {
|
||||
super(name || 'export');
|
||||
|
||||
// Initialize auth manager
|
||||
this.authManager = AuthManager.getInstance();
|
||||
|
||||
// Configure the command
|
||||
this.description('Export tasks to external systems (e.g., Hamster briefs)');
|
||||
|
||||
// Add options
|
||||
this.option('--org <id>', 'Organization ID to export to');
|
||||
this.option('--brief <id>', 'Brief ID to export tasks to');
|
||||
this.option('--tag <tag>', 'Export tasks from a specific tag');
|
||||
this.option(
|
||||
'--status <status>',
|
||||
'Filter tasks by status (pending, in-progress, done, etc.)'
|
||||
);
|
||||
this.option('--exclude-subtasks', 'Exclude subtasks from export');
|
||||
this.option('-y, --yes', 'Skip confirmation prompt');
|
||||
|
||||
// Accept optional positional argument for brief ID or Hamster URL
|
||||
this.argument('[briefOrUrl]', 'Brief ID or Hamster brief URL');
|
||||
|
||||
// Default action
|
||||
this.action(async (briefOrUrl?: string, options?: any) => {
|
||||
await this.executeExport(briefOrUrl, options);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the TaskMasterCore
|
||||
*/
|
||||
private async initializeServices(): Promise<void> {
|
||||
if (this.taskMasterCore) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Initialize TaskMasterCore
|
||||
this.taskMasterCore = await TaskMasterCore.create({
|
||||
projectPath: process.cwd()
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to initialize services: ${(error as Error).message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the export command
|
||||
*/
|
||||
private async executeExport(
|
||||
briefOrUrl?: string,
|
||||
options?: any
|
||||
): Promise<void> {
|
||||
let spinner: Ora | undefined;
|
||||
|
||||
try {
|
||||
// Check authentication
|
||||
if (!this.authManager.isAuthenticated()) {
|
||||
ui.displayError('Not authenticated. Run "tm auth login" first.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Initialize services
|
||||
await this.initializeServices();
|
||||
|
||||
// Get current context
|
||||
const context = this.authManager.getContext();
|
||||
|
||||
// Determine org and brief IDs
|
||||
let orgId = options?.org || context?.orgId;
|
||||
let briefId = options?.brief || briefOrUrl || context?.briefId;
|
||||
|
||||
// If a URL/ID was provided as argument, resolve it
|
||||
if (briefOrUrl && !options?.brief) {
|
||||
spinner = ora('Resolving brief...').start();
|
||||
const resolvedBrief = await this.resolveBriefInput(briefOrUrl);
|
||||
if (resolvedBrief) {
|
||||
briefId = resolvedBrief.briefId;
|
||||
orgId = resolvedBrief.orgId;
|
||||
spinner.succeed('Brief resolved');
|
||||
} else {
|
||||
spinner.fail('Could not resolve brief');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate we have necessary IDs
|
||||
if (!orgId) {
|
||||
ui.displayError(
|
||||
'No organization selected. Run "tm context org" or use --org flag.'
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!briefId) {
|
||||
ui.displayError(
|
||||
'No brief specified. Run "tm context brief", provide a brief ID/URL, or use --brief flag.'
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Confirm export if not auto-confirmed
|
||||
if (!options?.yes) {
|
||||
const confirmed = await this.confirmExport(orgId, briefId, context);
|
||||
if (!confirmed) {
|
||||
ui.displayWarning('Export cancelled');
|
||||
this.lastResult = {
|
||||
success: false,
|
||||
action: 'cancelled',
|
||||
message: 'User cancelled export'
|
||||
};
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Perform export
|
||||
spinner = ora('Exporting tasks...').start();
|
||||
|
||||
const exportResult = await this.taskMasterCore!.exportTasks({
|
||||
orgId,
|
||||
briefId,
|
||||
tag: options?.tag,
|
||||
status: options?.status,
|
||||
excludeSubtasks: options?.excludeSubtasks || false
|
||||
});
|
||||
|
||||
if (exportResult.success) {
|
||||
spinner.succeed(
|
||||
`Successfully exported ${exportResult.taskCount} task(s) to brief`
|
||||
);
|
||||
|
||||
// Display summary
|
||||
console.log(chalk.cyan('\n📤 Export Summary\n'));
|
||||
console.log(chalk.white(` Organization: ${orgId}`));
|
||||
console.log(chalk.white(` Brief: ${briefId}`));
|
||||
console.log(chalk.white(` Tasks exported: ${exportResult.taskCount}`));
|
||||
if (options?.tag) {
|
||||
console.log(chalk.gray(` Tag: ${options.tag}`));
|
||||
}
|
||||
if (options?.status) {
|
||||
console.log(chalk.gray(` Status filter: ${options.status}`));
|
||||
}
|
||||
|
||||
if (exportResult.message) {
|
||||
console.log(chalk.gray(`\n ${exportResult.message}`));
|
||||
}
|
||||
} else {
|
||||
spinner.fail('Export failed');
|
||||
if (exportResult.error) {
|
||||
console.error(chalk.red(`\n✗ ${exportResult.error.message}`));
|
||||
}
|
||||
}
|
||||
|
||||
this.lastResult = {
|
||||
success: exportResult.success,
|
||||
action: 'export',
|
||||
result: exportResult
|
||||
};
|
||||
} catch (error: any) {
|
||||
if (spinner?.isSpinning) spinner.fail('Export failed');
|
||||
this.handleError(error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve brief input to get brief and org IDs
|
||||
*/
|
||||
private async resolveBriefInput(
|
||||
briefOrUrl: string
|
||||
): Promise<{ briefId: string; orgId: string } | null> {
|
||||
try {
|
||||
// Extract brief ID from input
|
||||
const briefId = this.extractBriefId(briefOrUrl);
|
||||
if (!briefId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Fetch brief to get organization
|
||||
const brief = await this.authManager.getBrief(briefId);
|
||||
if (!brief) {
|
||||
ui.displayError('Brief not found or you do not have access');
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
briefId: brief.id,
|
||||
orgId: brief.accountId
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Failed to resolve brief: ${error}`));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a brief ID from raw input (ID or URL)
|
||||
*/
|
||||
private extractBriefId(input: string): string | null {
|
||||
const raw = input?.trim() ?? '';
|
||||
if (!raw) return null;
|
||||
|
||||
const parseUrl = (s: string): URL | null => {
|
||||
try {
|
||||
return new URL(s);
|
||||
} catch {}
|
||||
try {
|
||||
return new URL(`https://${s}`);
|
||||
} catch {}
|
||||
return null;
|
||||
};
|
||||
|
||||
const fromParts = (path: string): string | null => {
|
||||
const parts = path.split('/').filter(Boolean);
|
||||
const briefsIdx = parts.lastIndexOf('briefs');
|
||||
const candidate =
|
||||
briefsIdx >= 0 && parts.length > briefsIdx + 1
|
||||
? parts[briefsIdx + 1]
|
||||
: parts[parts.length - 1];
|
||||
return candidate?.trim() || null;
|
||||
};
|
||||
|
||||
// Try URL parsing
|
||||
const url = parseUrl(raw);
|
||||
if (url) {
|
||||
const qId = url.searchParams.get('id') || url.searchParams.get('briefId');
|
||||
const candidate = (qId || fromParts(url.pathname)) ?? null;
|
||||
if (candidate) {
|
||||
if (this.isLikelyId(candidate) || candidate.length >= 8) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if it looks like a path
|
||||
if (raw.includes('/')) {
|
||||
const candidate = fromParts(raw);
|
||||
if (candidate && (this.isLikelyId(candidate) || candidate.length >= 8)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
// Return raw if it looks like an ID
|
||||
return raw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a string looks like a brief ID
|
||||
*/
|
||||
private isLikelyId(value: string): boolean {
|
||||
const uuidRegex =
|
||||
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
||||
const ulidRegex = /^[0-9A-HJKMNP-TV-Z]{26}$/i;
|
||||
const slugRegex = /^[A-Za-z0-9_-]{16,}$/;
|
||||
return (
|
||||
uuidRegex.test(value) || ulidRegex.test(value) || slugRegex.test(value)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm export with the user
|
||||
*/
|
||||
private async confirmExport(
|
||||
orgId: string,
|
||||
briefId: string,
|
||||
context: UserContext | null
|
||||
): Promise<boolean> {
|
||||
console.log(chalk.cyan('\n📤 Export Tasks\n'));
|
||||
|
||||
// Show org name if available
|
||||
if (context?.orgName) {
|
||||
console.log(chalk.white(` Organization: ${context.orgName}`));
|
||||
console.log(chalk.gray(` ID: ${orgId}`));
|
||||
} else {
|
||||
console.log(chalk.white(` Organization ID: ${orgId}`));
|
||||
}
|
||||
|
||||
// Show brief info
|
||||
if (context?.briefName) {
|
||||
console.log(chalk.white(`\n Brief: ${context.briefName}`));
|
||||
console.log(chalk.gray(` ID: ${briefId}`));
|
||||
} else {
|
||||
console.log(chalk.white(`\n Brief ID: ${briefId}`));
|
||||
}
|
||||
|
||||
const { confirmed } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'confirmed',
|
||||
message: 'Do you want to proceed with export?',
|
||||
default: true
|
||||
}
|
||||
]);
|
||||
|
||||
return confirmed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle errors
|
||||
*/
|
||||
private handleError(error: any): void {
|
||||
if (error instanceof AuthenticationError) {
|
||||
console.error(chalk.red(`\n✗ ${error.message}`));
|
||||
|
||||
if (error.code === 'NOT_AUTHENTICATED') {
|
||||
ui.displayWarning('Please authenticate first: tm auth login');
|
||||
}
|
||||
} else {
|
||||
const msg = error?.message ?? String(error);
|
||||
console.error(chalk.red(`Error: ${msg}`));
|
||||
|
||||
if (error.stack && process.env.DEBUG) {
|
||||
console.error(chalk.gray(error.stack));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last export result (useful for testing)
|
||||
*/
|
||||
public getLastResult(): ExportCommandResult | undefined {
|
||||
return this.lastResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up resources
|
||||
*/
|
||||
async cleanup(): Promise<void> {
|
||||
// No resources to clean up
|
||||
}
|
||||
|
||||
/**
|
||||
* Register this command on an existing program
|
||||
*/
|
||||
static register(program: Command, name?: string): ExportCommand {
|
||||
const exportCommand = new ExportCommand(name);
|
||||
program.addCommand(exportCommand);
|
||||
return exportCommand;
|
||||
}
|
||||
}
|
||||
@@ -246,7 +246,7 @@ export class ListTasksCommand extends Command {
|
||||
task.subtasks.forEach((subtask) => {
|
||||
const subIcon = STATUS_ICONS[subtask.status];
|
||||
console.log(
|
||||
` ${chalk.gray(`${task.id}.${subtask.id}`)} ${subIcon} ${chalk.gray(subtask.title)}`
|
||||
` ${chalk.gray(String(subtask.id))} ${subIcon} ${chalk.gray(subtask.title)}`
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -297,7 +297,7 @@ export class ListTasksCommand extends Command {
|
||||
nextTask
|
||||
);
|
||||
|
||||
// Task table - no title, just show the table directly
|
||||
// Task table
|
||||
console.log(
|
||||
ui.createTaskTable(tasks, {
|
||||
showSubtasks: withSubtasks,
|
||||
@@ -474,18 +474,7 @@ export class ListTasksCommand extends Command {
|
||||
}
|
||||
|
||||
/**
|
||||
* Static method to register this command on an existing program
|
||||
* This is for gradual migration - allows commands.js to use this
|
||||
*/
|
||||
static registerOn(program: Command): Command {
|
||||
const listCommand = new ListTasksCommand();
|
||||
program.addCommand(listCommand);
|
||||
return listCommand;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alternative registration that returns the command for chaining
|
||||
* Can also configure the command name if needed
|
||||
* Register this command on an existing program
|
||||
*/
|
||||
static register(program: Command, name?: string): ListTasksCommand {
|
||||
const listCommand = new ListTasksCommand(name);
|
||||
|
||||
@@ -258,9 +258,6 @@ export class SetStatusCommand extends Command {
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Show storage info
|
||||
console.log(chalk.gray(`\nUsing ${result.storageType} storage`));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -290,18 +287,7 @@ export class SetStatusCommand extends Command {
|
||||
}
|
||||
|
||||
/**
|
||||
* Static method to register this command on an existing program
|
||||
* This is for gradual migration - allows commands.js to use this
|
||||
*/
|
||||
static registerOn(program: Command): Command {
|
||||
const setStatusCommand = new SetStatusCommand();
|
||||
program.addCommand(setStatusCommand);
|
||||
return setStatusCommand;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alternative registration that returns the command for chaining
|
||||
* Can also configure the command name if needed
|
||||
* Register this command on an existing program
|
||||
*/
|
||||
static register(program: Command, name?: string): SetStatusCommand {
|
||||
const setStatusCommand = new SetStatusCommand(name);
|
||||
|
||||
@@ -322,18 +322,7 @@ export class ShowCommand extends Command {
|
||||
}
|
||||
|
||||
/**
|
||||
* Static method to register this command on an existing program
|
||||
* This is for gradual migration - allows commands.js to use this
|
||||
*/
|
||||
static registerOn(program: Command): Command {
|
||||
const showCommand = new ShowCommand();
|
||||
program.addCommand(showCommand);
|
||||
return showCommand;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alternative registration that returns the command for chaining
|
||||
* Can also configure the command name if needed
|
||||
* Register this command on an existing program
|
||||
*/
|
||||
static register(program: Command, name?: string): ShowCommand {
|
||||
const showCommand = new ShowCommand(name);
|
||||
|
||||
@@ -493,16 +493,7 @@ export class StartCommand extends Command {
|
||||
}
|
||||
|
||||
/**
|
||||
* Static method to register this command on an existing program
|
||||
*/
|
||||
static registerOn(program: Command): Command {
|
||||
const startCommand = new StartCommand();
|
||||
program.addCommand(startCommand);
|
||||
return startCommand;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alternative registration that returns the command for chaining
|
||||
* Register this command on an existing program
|
||||
*/
|
||||
static register(program: Command, name?: string): StartCommand {
|
||||
const startCommand = new StartCommand(name);
|
||||
|
||||
@@ -10,6 +10,15 @@ export { AuthCommand } from './commands/auth.command.js';
|
||||
export { ContextCommand } from './commands/context.command.js';
|
||||
export { StartCommand } from './commands/start.command.js';
|
||||
export { SetStatusCommand } from './commands/set-status.command.js';
|
||||
export { ExportCommand } from './commands/export.command.js';
|
||||
|
||||
// Command Registry
|
||||
export {
|
||||
CommandRegistry,
|
||||
registerAllCommands,
|
||||
registerCommandsByCategory,
|
||||
type CommandMetadata
|
||||
} from './command-registry.js';
|
||||
|
||||
// UI utilities (for other commands to use)
|
||||
export * as ui from './utils/ui.js';
|
||||
|
||||
@@ -192,8 +192,7 @@ export function displaySubtasks(
|
||||
status: any;
|
||||
description?: string;
|
||||
dependencies?: string[];
|
||||
}>,
|
||||
parentId: string | number
|
||||
}>
|
||||
): void {
|
||||
const terminalWidth = process.stdout.columns * 0.95 || 100;
|
||||
// Display subtasks header
|
||||
@@ -228,7 +227,7 @@ export function displaySubtasks(
|
||||
});
|
||||
|
||||
subtasks.forEach((subtask) => {
|
||||
const subtaskId = `${parentId}.${subtask.id}`;
|
||||
const subtaskId = String(subtask.id);
|
||||
|
||||
// Format dependencies
|
||||
const deps =
|
||||
@@ -329,7 +328,7 @@ export function displayTaskDetails(
|
||||
console.log(chalk.gray(` No subtasks with status '${statusFilter}'`));
|
||||
} else if (filteredSubtasks.length > 0) {
|
||||
console.log(); // Empty line for spacing
|
||||
displaySubtasks(filteredSubtasks, task.id);
|
||||
displaySubtasks(filteredSubtasks);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -286,12 +286,12 @@ export function createTaskTable(
|
||||
// Adjust column widths to better match the original layout
|
||||
const baseColWidths = showComplexity
|
||||
? [
|
||||
Math.floor(terminalWidth * 0.06),
|
||||
Math.floor(terminalWidth * 0.1),
|
||||
Math.floor(terminalWidth * 0.4),
|
||||
Math.floor(terminalWidth * 0.15),
|
||||
Math.floor(terminalWidth * 0.12),
|
||||
Math.floor(terminalWidth * 0.1),
|
||||
Math.floor(terminalWidth * 0.2),
|
||||
Math.floor(terminalWidth * 0.12)
|
||||
Math.floor(terminalWidth * 0.1)
|
||||
] // ID, Title, Status, Priority, Dependencies, Complexity
|
||||
: [
|
||||
Math.floor(terminalWidth * 0.08),
|
||||
@@ -377,7 +377,11 @@ export function createTaskTable(
|
||||
}
|
||||
|
||||
if (showComplexity) {
|
||||
subRow.push(chalk.gray('--'));
|
||||
const complexityDisplay =
|
||||
typeof subtask.complexity === 'number'
|
||||
? getComplexityWithColor(subtask.complexity)
|
||||
: '--';
|
||||
subRow.push(chalk.gray(complexityDisplay));
|
||||
}
|
||||
|
||||
table.push(subRow);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# docs
|
||||
|
||||
## 0.0.5
|
||||
|
||||
## 0.0.4
|
||||
|
||||
## 0.0.3
|
||||
|
||||
@@ -75,7 +75,6 @@ Taskmaster uses two primary methods for configuration:
|
||||
- `AZURE_OPENAI_API_KEY`: Your Azure OpenAI API key (also requires `AZURE_OPENAI_ENDPOINT`).
|
||||
- `OPENROUTER_API_KEY`: Your OpenRouter API key.
|
||||
- `XAI_API_KEY`: Your X-AI API key.
|
||||
- `OPENAI_CODEX_API_KEY`: Your OpenAI API key for Codex CLI (optional - OAuth preferred).
|
||||
- **Optional Endpoint Overrides:**
|
||||
- **Per-role `baseURL` in `.taskmasterconfig`:** You can add a `baseURL` property to any model role (`main`, `research`, `fallback`) to override the default API endpoint for that provider. If omitted, the provider's standard endpoint is used.
|
||||
- **Environment Variable Overrides (`<PROVIDER>_BASE_URL`):** For greater flexibility, especially with third-party services, you can set an environment variable like `OPENAI_BASE_URL` or `MISTRAL_BASE_URL`. This will override any `baseURL` set in the configuration file for that provider. This is the recommended way to connect to OpenAI-compatible APIs.
|
||||
@@ -317,89 +316,4 @@ Azure OpenAI provides enterprise-grade OpenAI models through Microsoft's Azure c
|
||||
- Confirm the model is deployed in your Azure OpenAI resource
|
||||
- Verify the deployment name matches your configuration exactly (case-sensitive)
|
||||
- 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.
|
||||
|
||||
### Codex CLI Configuration
|
||||
|
||||
The Codex CLI provider integrates with OpenAI's ChatGPT subscription through the Codex CLI tool, providing access to advanced models like GPT-5 and GPT-5-Codex.
|
||||
|
||||
1. **Prerequisites**:
|
||||
- Node.js >= 18.0.0
|
||||
- Codex CLI >= 0.42.0 (>= 0.44.0 recommended)
|
||||
- Active ChatGPT subscription (Plus, Pro, Business, Edu, or Enterprise)
|
||||
|
||||
2. **Installation**:
|
||||
```bash
|
||||
# Install Codex CLI globally
|
||||
npm install -g @openai/codex
|
||||
|
||||
# Authenticate with your ChatGPT account
|
||||
codex login
|
||||
```
|
||||
|
||||
3. **Configuration Options**:
|
||||
|
||||
**Basic Configuration**
|
||||
```json
|
||||
// In .taskmaster/config.json
|
||||
{
|
||||
"models": {
|
||||
"main": {
|
||||
"provider": "codex-cli",
|
||||
"modelId": "gpt-5-codex",
|
||||
"maxTokens": 128000,
|
||||
"temperature": 0.2
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Advanced Configuration with Codex CLI Settings**
|
||||
```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,
|
||||
"commandSpecific": {
|
||||
"parse-prd": {
|
||||
"approvalMode": "never",
|
||||
"verbose": true
|
||||
},
|
||||
"expand": {
|
||||
"sandboxMode": "read-only"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. **Authentication**:
|
||||
- **Primary Method**: OAuth authentication via `codex login` (recommended)
|
||||
- **Optional**: Set `OPENAI_CODEX_API_KEY` environment variable as fallback
|
||||
- Note: API key doesn't provide access to subscription-only models like GPT-5-Codex
|
||||
|
||||
5. **Available Settings**:
|
||||
- **`allowNpx`** (boolean): Allow fallback to `npx @openai/codex` if CLI not found
|
||||
- **`skipGitRepoCheck`** (boolean): Skip git repository safety check
|
||||
- **`approvalMode`** (string): Control command approval (`"untrusted"`, `"on-failure"`, `"on-request"`, `"never"`)
|
||||
- **`sandboxMode`** (string): Control filesystem access (`"read-only"`, `"workspace-write"`, `"danger-full-access"`)
|
||||
- **`codexPath`** (string): Custom path to Codex CLI executable
|
||||
- **`cwd`** (string): Working directory for execution
|
||||
- **`verbose`** (boolean): Enable verbose logging
|
||||
- **`commandSpecific`** (object): Override settings for specific commands
|
||||
|
||||
6. **Security Notes**:
|
||||
- Default settings prioritize safety with approval and sandbox modes
|
||||
- Use `fullAuto: true` or `dangerouslyBypassApprovalsAndSandbox: true` with extreme caution
|
||||
- Codebase analysis is automatically enabled for this provider
|
||||
- Ensure youre not getting rate limited by `maxTokens` maintain appropriate Tokens per Minute Rate Limit (TPM) in your deployment.
|
||||
@@ -52,18 +52,6 @@
|
||||
"capabilities/cli-root-commands",
|
||||
"capabilities/task-structure"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "AI Providers",
|
||||
"pages": [
|
||||
"providers/codex-cli"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Usage Examples",
|
||||
"pages": [
|
||||
"examples/codex-cli-usage"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,466 +0,0 @@
|
||||
---
|
||||
title: Codex CLI Provider Usage Examples
|
||||
sidebarTitle: "Codex CLI Usage"
|
||||
---
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Read the [Codex CLI Provider Documentation](../providers/codex-cli)
|
||||
- Explore [Configuration Options](../best-practices/configuration-advanced)
|
||||
- Check out [API Keys Setup](../getting-started/api-keys)
|
||||
- Learn about [Task Structure](../capabilities/task-structure)
|
||||
|
||||
---
|
||||
|
||||
For more examples and advanced usage, see the [full documentation](https://docs.task-master.dev).
|
||||
@@ -112,19 +112,6 @@ AWS_ACCESS_KEY_ID="your-aws-access-key"
|
||||
AWS_SECRET_ACCESS_KEY="your-aws-secret-key"
|
||||
```
|
||||
|
||||
### OPENAI_CODEX_API_KEY
|
||||
- **Provider**: Codex CLI
|
||||
- **Required**: ❌ **No** (uses OAuth authentication)
|
||||
- **Purpose**: Optional API key for Codex CLI provider
|
||||
- **Authentication**: OAuth via `codex login` is preferred, API key is optional fallback
|
||||
- **Models**: GPT-5, GPT-5-Codex (subscription required)
|
||||
- **Get Key**: [OpenAI Platform](https://platform.openai.com/api-keys)
|
||||
|
||||
```bash
|
||||
# Optional - OAuth authentication is preferred
|
||||
OPENAI_CODEX_API_KEY="sk-your-openai-api-key-here"
|
||||
```
|
||||
|
||||
### CLAUDE_CODE_API_KEY
|
||||
- **Provider**: Claude Code CLI
|
||||
- **Required**: ❌ **No** (uses OAuth tokens)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "docs",
|
||||
"version": "0.0.4",
|
||||
"version": "0.0.5",
|
||||
"private": true,
|
||||
"description": "Task Master documentation powered by Mintlify",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,408 +0,0 @@
|
||||
---
|
||||
title: Codex CLI Provider
|
||||
sidebarTitle: "Codex CLI"
|
||||
---
|
||||
|
||||
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"`
|
||||
|
||||
#### Advanced Settings
|
||||
|
||||
- **`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"`
|
||||
|
||||
- **`verbose`** (boolean, optional)
|
||||
- Enable verbose provider logging
|
||||
- Helpful for debugging issues
|
||||
- Example: `"verbose": true`
|
||||
|
||||
- **`fullAuto`** (boolean, optional)
|
||||
- Fully automatic mode (equivalent to `--full-auto` flag)
|
||||
- Bypasses most approvals for fully automated workflows
|
||||
- Example: `"fullAuto": 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
|
||||
```
|
||||
|
||||
### "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"`
|
||||
|
||||
## 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](../best-practices/configuration-advanced) - Complete Codex CLI configuration reference
|
||||
- [API Keys](../getting-started/api-keys) - Setting up OPENAI_CODEX_API_KEY
|
||||
- [Codex CLI Usage Examples](../examples/codex-cli-usage) - Practical usage examples
|
||||
- [ai-sdk-provider-codex-cli](https://github.com/ben-vargas/ai-sdk-provider-codex-cli) - Source code for the provider package
|
||||
@@ -1,5 +1,12 @@
|
||||
# Change Log
|
||||
|
||||
## 0.25.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`b43b7ce`](https://github.com/eyaltoledano/claude-task-master/commit/b43b7ce201625eee956fb2f8cd332f238bb78c21), [`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), [`4f984f8`](https://github.com/eyaltoledano/claude-task-master/commit/4f984f8a6965da9f9c7edd60ddfd6560ac022917), [`7b5a7c4`](https://github.com/eyaltoledano/claude-task-master/commit/7b5a7c4495a68b782f7407fc5d0e0d3ae81f42f5), [`caee040`](https://github.com/eyaltoledano/claude-task-master/commit/caee040907f856d31a660171c9e6d966f23c632e), [`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
|
||||
|
||||
## 0.25.5-rc.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"private": true,
|
||||
"displayName": "TaskMaster",
|
||||
"description": "A visual Kanban board interface for TaskMaster projects in VS Code",
|
||||
"version": "0.25.5-rc.0",
|
||||
"version": "0.25.5",
|
||||
"publisher": "Hamster",
|
||||
"icon": "assets/icon.png",
|
||||
"engines": {
|
||||
|
||||
@@ -69,11 +69,29 @@ export function resolveTasksPath(args, log = silentLogger) {
|
||||
|
||||
// Use core findTasksPath with explicit path and normalized projectRoot context
|
||||
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
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
67
output.txt
67
output.txt
File diff suppressed because one or more lines are too long
43
package-lock.json
generated
43
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "task-master-ai",
|
||||
"version": "0.27.3",
|
||||
"version": "0.28.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "task-master-ai",
|
||||
"version": "0.27.3",
|
||||
"version": "0.28.0",
|
||||
"license": "MIT WITH Commons-Clause",
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
@@ -125,13 +125,13 @@
|
||||
}
|
||||
},
|
||||
"apps/docs": {
|
||||
"version": "0.0.4",
|
||||
"version": "0.0.5",
|
||||
"devDependencies": {
|
||||
"mintlify": "^4.2.111"
|
||||
}
|
||||
},
|
||||
"apps/extension": {
|
||||
"version": "0.25.4",
|
||||
"version": "0.25.5",
|
||||
"dependencies": {
|
||||
"task-master-ai": "*"
|
||||
},
|
||||
@@ -635,7 +635,6 @@
|
||||
"apps/extension/node_modules/zod": {
|
||||
"version": "3.25.76",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
@@ -1830,7 +1829,6 @@
|
||||
"version": "7.28.4",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.3",
|
||||
@@ -2663,7 +2661,6 @@
|
||||
"version": "6.3.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@dnd-kit/accessibility": "^3.1.1",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
@@ -4583,6 +4580,7 @@
|
||||
"version": "0.23.2",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
}
|
||||
@@ -5172,6 +5170,7 @@
|
||||
"version": "0.23.2",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
}
|
||||
@@ -5180,7 +5179,6 @@
|
||||
"version": "3.25.76",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
@@ -5471,7 +5469,6 @@
|
||||
"node_modules/@modelcontextprotocol/sdk/node_modules/zod": {
|
||||
"version": "3.25.76",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
@@ -5572,7 +5569,6 @@
|
||||
"node_modules/@opentelemetry/api": {
|
||||
"version": "1.9.0",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
@@ -8592,7 +8588,6 @@
|
||||
"version": "19.1.8",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
@@ -8601,7 +8596,6 @@
|
||||
"version": "19.1.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.0.0"
|
||||
}
|
||||
@@ -9047,7 +9041,6 @@
|
||||
"node_modules/acorn": {
|
||||
"version": "8.15.0",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -9113,7 +9106,6 @@
|
||||
"node_modules/ai": {
|
||||
"version": "5.0.57",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@ai-sdk/gateway": "1.0.30",
|
||||
"@ai-sdk/provider": "2.0.0",
|
||||
@@ -9333,7 +9325,6 @@
|
||||
"node_modules/ajv": {
|
||||
"version": "8.17.1",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
@@ -10339,7 +10330,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.3",
|
||||
"caniuse-lite": "^1.0.30001741",
|
||||
@@ -12203,8 +12193,7 @@
|
||||
"node_modules/devtools-protocol": {
|
||||
"version": "0.0.1312386",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/dezalgo": {
|
||||
"version": "1.0.4",
|
||||
@@ -12798,7 +12787,6 @@
|
||||
"version": "0.25.10",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
@@ -13111,7 +13099,6 @@
|
||||
"node_modules/express": {
|
||||
"version": "4.21.2",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
@@ -15465,7 +15452,6 @@
|
||||
"version": "6.3.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@alcalzone/ansi-tokenize": "^0.2.0",
|
||||
"ansi-escapes": "^7.0.0",
|
||||
@@ -16423,7 +16409,6 @@
|
||||
"version": "29.7.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jest/core": "^29.7.0",
|
||||
"@jest/types": "^29.6.3",
|
||||
@@ -18041,7 +18026,6 @@
|
||||
"version": "1.4.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.16.0"
|
||||
}
|
||||
@@ -18367,6 +18351,7 @@
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
@@ -18591,6 +18576,7 @@
|
||||
"version": "1.4.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
},
|
||||
@@ -18721,7 +18707,6 @@
|
||||
"node_modules/marked": {
|
||||
"version": "15.0.12",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
@@ -21444,7 +21429,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -22827,7 +22811,6 @@
|
||||
"integrity": "sha512-U+NPR0Bkg3wm61dteD2L4nAM1U9dtaqVrpDXwC36IKRHpEO/Ubpid4Nijpa2imPchcVNHfxVFwSSMJdwdGFUbg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@oxc-project/types": "=0.93.0",
|
||||
"@rolldown/pluginutils": "1.0.0-beta.41",
|
||||
@@ -25256,7 +25239,6 @@
|
||||
"version": "5.9.2",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -25373,7 +25355,6 @@
|
||||
"version": "11.0.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/unist": "^3.0.0",
|
||||
"bail": "^2.0.0",
|
||||
@@ -25816,7 +25797,6 @@
|
||||
"version": "5.4.20",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
"postcss": "^8.4.43",
|
||||
@@ -25929,6 +25909,7 @@
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
@@ -26512,7 +26493,7 @@
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "1.10.2",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
@@ -26655,7 +26636,6 @@
|
||||
"node_modules/zod": {
|
||||
"version": "4.1.11",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
@@ -27397,7 +27377,6 @@
|
||||
"version": "3.2.4",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/expect": "3.2.4",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "task-master-ai",
|
||||
"version": "0.28.0-rc.1",
|
||||
"version": "0.28.0",
|
||||
"description": "A task management system for ambitious AI-driven development that doesn't overwhelm and confuse Cursor.",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
|
||||
3
packages/ai-sdk-provider-grok-cli/CHANGELOG.md
Normal file
3
packages/ai-sdk-provider-grok-cli/CHANGELOG.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# @tm/ai-sdk-provider-grok-cli
|
||||
|
||||
## null
|
||||
@@ -31,5 +31,6 @@
|
||||
"files": ["dist/**/*", "README.md"],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
},
|
||||
"version": ""
|
||||
}
|
||||
|
||||
@@ -2,4 +2,6 @@
|
||||
|
||||
## null
|
||||
|
||||
## null
|
||||
|
||||
## 1.0.1
|
||||
|
||||
@@ -24,5 +24,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"tsup": "^8.5.0"
|
||||
}
|
||||
},
|
||||
"version": ""
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
## null
|
||||
|
||||
## null
|
||||
|
||||
## 0.26.1
|
||||
|
||||
All notable changes to the @task-master/tm-core package will be documented in this file.
|
||||
|
||||
@@ -42,5 +42,6 @@
|
||||
"files": ["src", "README.md", "CHANGELOG.md"],
|
||||
"keywords": ["task-management", "typescript", "ai", "prd", "parser"],
|
||||
"author": "Task Master AI",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"version": ""
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ export class TaskEntity implements Task {
|
||||
// Normalize subtask IDs to strings
|
||||
this.subtasks = (data.subtasks || []).map((subtask) => ({
|
||||
...subtask,
|
||||
id: Number(subtask.id), // Keep subtask IDs as numbers per interface
|
||||
id: String(subtask.id),
|
||||
parentId: String(subtask.parentId)
|
||||
}));
|
||||
|
||||
|
||||
@@ -51,7 +51,8 @@ export const ERROR_CODES = {
|
||||
INTERNAL_ERROR: 'INTERNAL_ERROR',
|
||||
INVALID_INPUT: 'INVALID_INPUT',
|
||||
NOT_IMPLEMENTED: 'NOT_IMPLEMENTED',
|
||||
UNKNOWN_ERROR: 'UNKNOWN_ERROR'
|
||||
UNKNOWN_ERROR: 'UNKNOWN_ERROR',
|
||||
NOT_FOUND: 'NOT_FOUND'
|
||||
} as const;
|
||||
|
||||
export type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES];
|
||||
|
||||
@@ -11,7 +11,9 @@ export {
|
||||
type ListTasksResult,
|
||||
type StartTaskOptions,
|
||||
type StartTaskResult,
|
||||
type ConflictCheckResult
|
||||
type ConflictCheckResult,
|
||||
type ExportTasksOptions,
|
||||
type ExportResult
|
||||
} from './task-master-core.js';
|
||||
|
||||
// Re-export types
|
||||
|
||||
@@ -5,6 +5,16 @@
|
||||
|
||||
import type { Task, TaskMetadata, TaskStatus } from '../types/index.js';
|
||||
|
||||
/**
|
||||
* Options for loading tasks from storage
|
||||
*/
|
||||
export interface LoadTasksOptions {
|
||||
/** Filter tasks by status */
|
||||
status?: TaskStatus;
|
||||
/** Exclude subtasks from loaded tasks (default: false) */
|
||||
excludeSubtasks?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result type for updateTaskStatus operations
|
||||
*/
|
||||
@@ -21,11 +31,12 @@ export interface UpdateStatusResult {
|
||||
*/
|
||||
export interface IStorage {
|
||||
/**
|
||||
* Load all tasks from storage, optionally filtered by tag
|
||||
* Load all tasks from storage, optionally filtered by tag and other criteria
|
||||
* @param tag - Optional tag to filter tasks by
|
||||
* @param options - Optional filtering options (status, excludeSubtasks)
|
||||
* @returns Promise that resolves to an array of tasks
|
||||
*/
|
||||
loadTasks(tag?: string): Promise<Task[]>;
|
||||
loadTasks(tag?: string, options?: LoadTasksOptions): Promise<Task[]>;
|
||||
|
||||
/**
|
||||
* Load a single task by ID
|
||||
@@ -205,7 +216,7 @@ export abstract class BaseStorage implements IStorage {
|
||||
}
|
||||
|
||||
// Abstract methods that must be implemented by concrete classes
|
||||
abstract loadTasks(tag?: string): Promise<Task[]>;
|
||||
abstract loadTasks(tag?: string, options?: LoadTasksOptions): Promise<Task[]>;
|
||||
abstract loadTask(taskId: string, tag?: string): Promise<Task | null>;
|
||||
abstract saveTasks(tasks: Task[], tag?: string): Promise<void>;
|
||||
abstract appendTasks(tasks: Task[], tag?: string): Promise<void>;
|
||||
|
||||
148
packages/tm-core/src/mappers/TaskMapper.test.ts
Normal file
148
packages/tm-core/src/mappers/TaskMapper.test.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { TaskMapper } from './TaskMapper.js';
|
||||
import type { Tables } from '../types/database.types.js';
|
||||
|
||||
type TaskRow = Tables<'tasks'>;
|
||||
|
||||
describe('TaskMapper', () => {
|
||||
describe('extractMetadataField', () => {
|
||||
it('should extract string field from metadata', () => {
|
||||
const taskRow: TaskRow = {
|
||||
id: '123',
|
||||
display_id: '1',
|
||||
title: 'Test Task',
|
||||
description: 'Test description',
|
||||
status: 'todo',
|
||||
priority: 'medium',
|
||||
parent_task_id: null,
|
||||
subtask_position: 0,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
metadata: {
|
||||
details: 'Some details',
|
||||
testStrategy: 'Test with unit tests'
|
||||
},
|
||||
complexity: null,
|
||||
assignee_id: null,
|
||||
estimated_hours: null,
|
||||
actual_hours: null,
|
||||
due_date: null,
|
||||
completed_at: null
|
||||
};
|
||||
|
||||
const task = TaskMapper.mapDatabaseTaskToTask(taskRow, [], new Map());
|
||||
|
||||
expect(task.details).toBe('Some details');
|
||||
expect(task.testStrategy).toBe('Test with unit tests');
|
||||
});
|
||||
|
||||
it('should use default value when metadata field is missing', () => {
|
||||
const taskRow: TaskRow = {
|
||||
id: '123',
|
||||
display_id: '1',
|
||||
title: 'Test Task',
|
||||
description: 'Test description',
|
||||
status: 'todo',
|
||||
priority: 'medium',
|
||||
parent_task_id: null,
|
||||
subtask_position: 0,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
metadata: {},
|
||||
complexity: null,
|
||||
assignee_id: null,
|
||||
estimated_hours: null,
|
||||
actual_hours: null,
|
||||
due_date: null,
|
||||
completed_at: null
|
||||
};
|
||||
|
||||
const task = TaskMapper.mapDatabaseTaskToTask(taskRow, [], new Map());
|
||||
|
||||
expect(task.details).toBe('');
|
||||
expect(task.testStrategy).toBe('');
|
||||
});
|
||||
|
||||
it('should use default value when metadata is null', () => {
|
||||
const taskRow: TaskRow = {
|
||||
id: '123',
|
||||
display_id: '1',
|
||||
title: 'Test Task',
|
||||
description: 'Test description',
|
||||
status: 'todo',
|
||||
priority: 'medium',
|
||||
parent_task_id: null,
|
||||
subtask_position: 0,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
metadata: null,
|
||||
complexity: null,
|
||||
assignee_id: null,
|
||||
estimated_hours: null,
|
||||
actual_hours: null,
|
||||
due_date: null,
|
||||
completed_at: null
|
||||
};
|
||||
|
||||
const task = TaskMapper.mapDatabaseTaskToTask(taskRow, [], new Map());
|
||||
|
||||
expect(task.details).toBe('');
|
||||
expect(task.testStrategy).toBe('');
|
||||
});
|
||||
|
||||
it('should use default value and warn when metadata field has wrong type', () => {
|
||||
const consoleWarnSpy = vi
|
||||
.spyOn(console, 'warn')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
const taskRow: TaskRow = {
|
||||
id: '123',
|
||||
display_id: '1',
|
||||
title: 'Test Task',
|
||||
description: 'Test description',
|
||||
status: 'todo',
|
||||
priority: 'medium',
|
||||
parent_task_id: null,
|
||||
subtask_position: 0,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
metadata: {
|
||||
details: 12345, // Wrong type: number instead of string
|
||||
testStrategy: ['test1', 'test2'] // Wrong type: array instead of string
|
||||
},
|
||||
complexity: null,
|
||||
assignee_id: null,
|
||||
estimated_hours: null,
|
||||
actual_hours: null,
|
||||
due_date: null,
|
||||
completed_at: null
|
||||
};
|
||||
|
||||
const task = TaskMapper.mapDatabaseTaskToTask(taskRow, [], new Map());
|
||||
|
||||
// Should use empty string defaults when type doesn't match
|
||||
expect(task.details).toBe('');
|
||||
expect(task.testStrategy).toBe('');
|
||||
|
||||
// Should have logged warnings
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Type mismatch in metadata field "details"')
|
||||
);
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'Type mismatch in metadata field "testStrategy"'
|
||||
)
|
||||
);
|
||||
|
||||
consoleWarnSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapStatus', () => {
|
||||
it('should map database status to internal status', () => {
|
||||
expect(TaskMapper.mapStatus('todo')).toBe('pending');
|
||||
expect(TaskMapper.mapStatus('in_progress')).toBe('in-progress');
|
||||
expect(TaskMapper.mapStatus('done')).toBe('done');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2,22 +2,32 @@ import { Task, Subtask } from '../types/index.js';
|
||||
import { Database, Tables } from '../types/database.types.js';
|
||||
|
||||
type TaskRow = Tables<'tasks'>;
|
||||
type DependencyRow = Tables<'task_dependencies'>;
|
||||
|
||||
// Legacy type for backward compatibility
|
||||
type DependencyRow = Tables<'task_dependencies'> & {
|
||||
depends_on_task?: { display_id: string } | null;
|
||||
depends_on_task_id?: string;
|
||||
};
|
||||
|
||||
export class TaskMapper {
|
||||
/**
|
||||
* Maps database tasks to internal Task format
|
||||
* @param dbTasks - Array of tasks from database
|
||||
* @param dependencies - Either a Map of task_id to display_ids or legacy array format
|
||||
*/
|
||||
static mapDatabaseTasksToTasks(
|
||||
dbTasks: TaskRow[],
|
||||
dbDependencies: DependencyRow[]
|
||||
dependencies: Map<string, string[]> | DependencyRow[]
|
||||
): Task[] {
|
||||
if (!dbTasks || dbTasks.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Group dependencies by task_id
|
||||
const dependenciesByTaskId = this.groupDependenciesByTaskId(dbDependencies);
|
||||
// Handle both Map and array formats for backward compatibility
|
||||
const dependenciesByTaskId =
|
||||
dependencies instanceof Map
|
||||
? dependencies
|
||||
: this.groupDependenciesByTaskId(dependencies);
|
||||
|
||||
// Separate parent tasks and subtasks
|
||||
const parentTasks = dbTasks.filter((t) => !t.parent_task_id);
|
||||
@@ -43,21 +53,23 @@ export class TaskMapper {
|
||||
): Task {
|
||||
// Map subtasks
|
||||
const subtasks: Subtask[] = dbSubtasks.map((subtask, index) => ({
|
||||
id: index + 1, // Use numeric ID for subtasks
|
||||
id: subtask.display_id || String(index + 1), // Use display_id if available (API storage), fallback to numeric (file storage)
|
||||
parentId: dbTask.id,
|
||||
title: subtask.title,
|
||||
description: subtask.description || '',
|
||||
status: this.mapStatus(subtask.status),
|
||||
priority: this.mapPriority(subtask.priority),
|
||||
dependencies: dependenciesByTaskId.get(subtask.id) || [],
|
||||
details: (subtask.metadata as any)?.details || '',
|
||||
testStrategy: (subtask.metadata as any)?.testStrategy || '',
|
||||
details: this.extractMetadataField(subtask.metadata, 'details', ''),
|
||||
testStrategy: this.extractMetadataField(
|
||||
subtask.metadata,
|
||||
'testStrategy',
|
||||
''
|
||||
),
|
||||
createdAt: subtask.created_at,
|
||||
updatedAt: subtask.updated_at,
|
||||
assignee: subtask.assignee_id || undefined,
|
||||
complexity: subtask.complexity
|
||||
? this.mapComplexityToInternal(subtask.complexity)
|
||||
: undefined
|
||||
complexity: subtask.complexity ?? undefined
|
||||
}));
|
||||
|
||||
return {
|
||||
@@ -67,22 +79,25 @@ export class TaskMapper {
|
||||
status: this.mapStatus(dbTask.status),
|
||||
priority: this.mapPriority(dbTask.priority),
|
||||
dependencies: dependenciesByTaskId.get(dbTask.id) || [],
|
||||
details: (dbTask.metadata as any)?.details || '',
|
||||
testStrategy: (dbTask.metadata as any)?.testStrategy || '',
|
||||
details: this.extractMetadataField(dbTask.metadata, 'details', ''),
|
||||
testStrategy: this.extractMetadataField(
|
||||
dbTask.metadata,
|
||||
'testStrategy',
|
||||
''
|
||||
),
|
||||
subtasks,
|
||||
createdAt: dbTask.created_at,
|
||||
updatedAt: dbTask.updated_at,
|
||||
assignee: dbTask.assignee_id || undefined,
|
||||
complexity: dbTask.complexity
|
||||
? this.mapComplexityToInternal(dbTask.complexity)
|
||||
: undefined,
|
||||
complexity: dbTask.complexity ?? undefined,
|
||||
effort: dbTask.estimated_hours || undefined,
|
||||
actualEffort: dbTask.actual_hours || undefined
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Groups dependencies by task ID
|
||||
* Groups dependencies by task ID (legacy method for backward compatibility)
|
||||
* @deprecated Use DependencyFetcher.fetchDependenciesWithDisplayIds instead
|
||||
*/
|
||||
private static groupDependenciesByTaskId(
|
||||
dependencies: DependencyRow[]
|
||||
@@ -92,7 +107,14 @@ export class TaskMapper {
|
||||
if (dependencies) {
|
||||
for (const dep of dependencies) {
|
||||
const deps = dependenciesByTaskId.get(dep.task_id) || [];
|
||||
deps.push(dep.depends_on_task_id);
|
||||
// Handle both old format (UUID string) and new format (object with display_id)
|
||||
const dependencyId =
|
||||
typeof dep.depends_on_task === 'object'
|
||||
? dep.depends_on_task?.display_id
|
||||
: dep.depends_on_task_id;
|
||||
if (dependencyId) {
|
||||
deps.push(dependencyId);
|
||||
}
|
||||
dependenciesByTaskId.set(dep.task_id, deps);
|
||||
}
|
||||
}
|
||||
@@ -157,14 +179,38 @@ export class TaskMapper {
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps numeric complexity to descriptive complexity
|
||||
* Safely extracts a field from metadata JSON with runtime type validation
|
||||
* @param metadata The metadata object (could be null or any type)
|
||||
* @param field The field to extract
|
||||
* @param defaultValue Default value if field doesn't exist
|
||||
* @returns The extracted value if it matches the expected type, otherwise defaultValue
|
||||
*/
|
||||
private static mapComplexityToInternal(
|
||||
complexity: number
|
||||
): Task['complexity'] {
|
||||
if (complexity <= 2) return 'simple';
|
||||
if (complexity <= 5) return 'moderate';
|
||||
if (complexity <= 8) return 'complex';
|
||||
return 'very-complex';
|
||||
private static extractMetadataField<T>(
|
||||
metadata: unknown,
|
||||
field: string,
|
||||
defaultValue: T
|
||||
): T {
|
||||
if (!metadata || typeof metadata !== 'object') {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
const value = (metadata as Record<string, unknown>)[field];
|
||||
|
||||
if (value === undefined) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
// Runtime type validation: ensure value matches the type of defaultValue
|
||||
const expectedType = typeof defaultValue;
|
||||
const actualType = typeof value;
|
||||
|
||||
if (expectedType !== actualType) {
|
||||
console.warn(
|
||||
`Type mismatch in metadata field "${field}": expected ${expectedType}, got ${actualType}. Using default value.`
|
||||
);
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return value as T;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,224 +0,0 @@
|
||||
import { SupabaseClient } from '@supabase/supabase-js';
|
||||
import { Task } from '../types/index.js';
|
||||
import { Database } from '../types/database.types.js';
|
||||
import { TaskMapper } from '../mappers/TaskMapper.js';
|
||||
import { AuthManager } from '../auth/auth-manager.js';
|
||||
import { z } from 'zod';
|
||||
|
||||
// Zod schema for task status validation
|
||||
const TaskStatusSchema = z.enum([
|
||||
'pending',
|
||||
'in-progress',
|
||||
'done',
|
||||
'review',
|
||||
'deferred',
|
||||
'cancelled',
|
||||
'blocked'
|
||||
]);
|
||||
|
||||
// Zod schema for task updates
|
||||
const TaskUpdateSchema = z
|
||||
.object({
|
||||
title: z.string().min(1).optional(),
|
||||
description: z.string().optional(),
|
||||
status: TaskStatusSchema.optional(),
|
||||
priority: z.enum(['low', 'medium', 'high', 'critical']).optional(),
|
||||
details: z.string().optional(),
|
||||
testStrategy: z.string().optional()
|
||||
})
|
||||
.partial();
|
||||
|
||||
export class SupabaseTaskRepository {
|
||||
constructor(private supabase: SupabaseClient<Database>) {}
|
||||
|
||||
async getTasks(_projectId?: string): Promise<Task[]> {
|
||||
// Get the current context to determine briefId
|
||||
const authManager = AuthManager.getInstance();
|
||||
const context = authManager.getContext();
|
||||
|
||||
if (!context || !context.briefId) {
|
||||
throw new Error(
|
||||
'No brief selected. Please select a brief first using: tm context brief'
|
||||
);
|
||||
}
|
||||
|
||||
// Get all tasks for the brief using the exact query structure
|
||||
const { data: tasks, error } = await this.supabase
|
||||
.from('tasks')
|
||||
.select(`
|
||||
*,
|
||||
document:document_id (
|
||||
id,
|
||||
document_name,
|
||||
title,
|
||||
description
|
||||
)
|
||||
`)
|
||||
.eq('brief_id', context.briefId)
|
||||
.order('position', { ascending: true })
|
||||
.order('subtask_position', { ascending: true })
|
||||
.order('created_at', { ascending: true });
|
||||
|
||||
if (error) {
|
||||
throw new Error(`Failed to fetch tasks: ${error.message}`);
|
||||
}
|
||||
|
||||
if (!tasks || tasks.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get all dependencies for these tasks
|
||||
const taskIds = tasks.map((t: any) => t.id);
|
||||
const { data: depsData, error: depsError } = await this.supabase
|
||||
.from('task_dependencies')
|
||||
.select('*')
|
||||
.in('task_id', taskIds);
|
||||
|
||||
if (depsError) {
|
||||
throw new Error(
|
||||
`Failed to fetch task dependencies: ${depsError.message}`
|
||||
);
|
||||
}
|
||||
|
||||
// Use mapper to convert to internal format
|
||||
return TaskMapper.mapDatabaseTasksToTasks(tasks, depsData || []);
|
||||
}
|
||||
|
||||
async getTask(_projectId: string, taskId: string): Promise<Task | null> {
|
||||
// Get the current context to determine briefId (projectId not used in Supabase context)
|
||||
const authManager = AuthManager.getInstance();
|
||||
const context = authManager.getContext();
|
||||
|
||||
if (!context || !context.briefId) {
|
||||
throw new Error(
|
||||
'No brief selected. Please select a brief first using: tm context brief'
|
||||
);
|
||||
}
|
||||
|
||||
const { data, error } = await this.supabase
|
||||
.from('tasks')
|
||||
.select('*')
|
||||
.eq('brief_id', context.briefId)
|
||||
.eq('display_id', taskId.toUpperCase())
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
if (error.code === 'PGRST116') {
|
||||
return null; // Not found
|
||||
}
|
||||
throw new Error(`Failed to fetch task: ${error.message}`);
|
||||
}
|
||||
|
||||
// Get dependencies for this task
|
||||
const { data: depsData } = await this.supabase
|
||||
.from('task_dependencies')
|
||||
.select('*')
|
||||
.eq('task_id', taskId);
|
||||
|
||||
// Get subtasks if this is a parent task
|
||||
const { data: subtasksData } = await this.supabase
|
||||
.from('tasks')
|
||||
.select('*')
|
||||
.eq('parent_task_id', taskId)
|
||||
.order('subtask_position', { ascending: true });
|
||||
|
||||
// Create dependency map
|
||||
const dependenciesByTaskId = new Map<string, string[]>();
|
||||
if (depsData) {
|
||||
dependenciesByTaskId.set(
|
||||
taskId,
|
||||
depsData.map(
|
||||
(d: Database['public']['Tables']['task_dependencies']['Row']) =>
|
||||
d.depends_on_task_id
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Use mapper to convert single task
|
||||
return TaskMapper.mapDatabaseTaskToTask(
|
||||
data,
|
||||
subtasksData || [],
|
||||
dependenciesByTaskId
|
||||
);
|
||||
}
|
||||
|
||||
async updateTask(
|
||||
projectId: string,
|
||||
taskId: string,
|
||||
updates: Partial<Task>
|
||||
): Promise<Task> {
|
||||
// Get the current context to determine briefId
|
||||
const authManager = AuthManager.getInstance();
|
||||
const context = authManager.getContext();
|
||||
|
||||
if (!context || !context.briefId) {
|
||||
throw new Error(
|
||||
'No brief selected. Please select a brief first using: tm context brief'
|
||||
);
|
||||
}
|
||||
|
||||
// Validate updates using Zod schema
|
||||
try {
|
||||
TaskUpdateSchema.parse(updates);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
const errorMessages = error.issues
|
||||
.map((err) => `${err.path.join('.')}: ${err.message}`)
|
||||
.join(', ');
|
||||
throw new Error(`Invalid task update data: ${errorMessages}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Convert Task fields to database fields - only include fields that actually exist in the database
|
||||
const dbUpdates: any = {};
|
||||
|
||||
if (updates.title !== undefined) dbUpdates.title = updates.title;
|
||||
if (updates.description !== undefined)
|
||||
dbUpdates.description = updates.description;
|
||||
if (updates.status !== undefined)
|
||||
dbUpdates.status = this.mapStatusToDatabase(updates.status);
|
||||
if (updates.priority !== undefined) dbUpdates.priority = updates.priority;
|
||||
// Skip fields that don't exist in database schema: details, testStrategy, etc.
|
||||
|
||||
// Update the task
|
||||
const { error } = await this.supabase
|
||||
.from('tasks')
|
||||
.update(dbUpdates)
|
||||
.eq('brief_id', context.briefId)
|
||||
.eq('display_id', taskId.toUpperCase());
|
||||
|
||||
if (error) {
|
||||
throw new Error(`Failed to update task: ${error.message}`);
|
||||
}
|
||||
|
||||
// Return the updated task by fetching it
|
||||
const updatedTask = await this.getTask(projectId, taskId);
|
||||
if (!updatedTask) {
|
||||
throw new Error(`Failed to retrieve updated task ${taskId}`);
|
||||
}
|
||||
|
||||
return updatedTask;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps internal status to database status
|
||||
*/
|
||||
private mapStatusToDatabase(
|
||||
status: string
|
||||
): Database['public']['Enums']['task_status'] {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return 'todo';
|
||||
case 'in-progress':
|
||||
case 'in_progress': // Accept both formats
|
||||
return 'in_progress';
|
||||
case 'done':
|
||||
return 'done';
|
||||
default:
|
||||
throw new Error(
|
||||
`Invalid task status: ${status}. Valid statuses are: pending, in-progress, done`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { SupabaseClient } from '@supabase/supabase-js';
|
||||
import { Database } from '../../types/database.types.js';
|
||||
import { DependencyWithDisplayId } from '../../types/repository-types.js';
|
||||
|
||||
/**
|
||||
* Handles fetching and processing of task dependencies with display_ids
|
||||
*/
|
||||
export class DependencyFetcher {
|
||||
constructor(private supabase: SupabaseClient<Database>) {}
|
||||
|
||||
/**
|
||||
* Fetches dependencies for given task IDs with display_ids joined
|
||||
* @param taskIds Array of task IDs to fetch dependencies for
|
||||
* @returns Map of task ID to array of dependency display_ids
|
||||
*/
|
||||
async fetchDependenciesWithDisplayIds(
|
||||
taskIds: string[]
|
||||
): Promise<Map<string, string[]>> {
|
||||
if (!taskIds || taskIds.length === 0) {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
const { data, error } = await this.supabase
|
||||
.from('task_dependencies')
|
||||
.select(`
|
||||
task_id,
|
||||
depends_on_task:tasks!task_dependencies_depends_on_task_id_fkey (
|
||||
display_id
|
||||
)
|
||||
`)
|
||||
.in('task_id', taskIds);
|
||||
|
||||
if (error) {
|
||||
throw new Error(`Failed to fetch task dependencies: ${error.message}`);
|
||||
}
|
||||
|
||||
return this.processDependencyData(data as DependencyWithDisplayId[]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes raw dependency data into a map structure
|
||||
*/
|
||||
private processDependencyData(
|
||||
dependencies: DependencyWithDisplayId[]
|
||||
): Map<string, string[]> {
|
||||
const dependenciesByTaskId = new Map<string, string[]>();
|
||||
|
||||
if (!dependencies) {
|
||||
return dependenciesByTaskId;
|
||||
}
|
||||
|
||||
for (const dep of dependencies) {
|
||||
if (!dep.task_id) continue;
|
||||
|
||||
const currentDeps = dependenciesByTaskId.get(dep.task_id) || [];
|
||||
|
||||
// Extract display_id from the joined object
|
||||
const displayId = dep.depends_on_task?.display_id;
|
||||
if (displayId) {
|
||||
currentDeps.push(displayId);
|
||||
}
|
||||
|
||||
dependenciesByTaskId.set(dep.task_id, currentDeps);
|
||||
}
|
||||
|
||||
return dependenciesByTaskId;
|
||||
}
|
||||
}
|
||||
5
packages/tm-core/src/repositories/supabase/index.ts
Normal file
5
packages/tm-core/src/repositories/supabase/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Supabase repository implementations
|
||||
*/
|
||||
export { SupabaseTaskRepository } from './supabase-task-repository.js';
|
||||
export { DependencyFetcher } from './dependency-fetcher.js';
|
||||
@@ -0,0 +1,275 @@
|
||||
import { SupabaseClient } from '@supabase/supabase-js';
|
||||
import { Task } from '../../types/index.js';
|
||||
import { Database, Json } from '../../types/database.types.js';
|
||||
import { TaskMapper } from '../../mappers/TaskMapper.js';
|
||||
import { AuthManager } from '../../auth/auth-manager.js';
|
||||
import { DependencyFetcher } from './dependency-fetcher.js';
|
||||
import {
|
||||
TaskWithRelations,
|
||||
TaskDatabaseUpdate
|
||||
} from '../../types/repository-types.js';
|
||||
import { LoadTasksOptions } from '../../interfaces/storage.interface.js';
|
||||
import { z } from 'zod';
|
||||
|
||||
// Zod schema for task status validation
|
||||
const TaskStatusSchema = z.enum([
|
||||
'pending',
|
||||
'in-progress',
|
||||
'done',
|
||||
'review',
|
||||
'deferred',
|
||||
'cancelled',
|
||||
'blocked'
|
||||
]);
|
||||
|
||||
// Zod schema for task updates
|
||||
const TaskUpdateSchema = z
|
||||
.object({
|
||||
title: z.string().min(1).optional(),
|
||||
description: z.string().optional(),
|
||||
status: TaskStatusSchema.optional(),
|
||||
priority: z.enum(['low', 'medium', 'high', 'critical']).optional(),
|
||||
details: z.string().optional(),
|
||||
testStrategy: z.string().optional()
|
||||
})
|
||||
.partial();
|
||||
|
||||
export class SupabaseTaskRepository {
|
||||
private dependencyFetcher: DependencyFetcher;
|
||||
private authManager: AuthManager;
|
||||
|
||||
constructor(private supabase: SupabaseClient<Database>) {
|
||||
this.dependencyFetcher = new DependencyFetcher(supabase);
|
||||
this.authManager = AuthManager.getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current brief ID from auth context
|
||||
* @throws {Error} If no brief is selected
|
||||
*/
|
||||
private getBriefIdOrThrow(): string {
|
||||
const context = this.authManager.getContext();
|
||||
if (!context?.briefId) {
|
||||
throw new Error(
|
||||
'No brief selected. Please select a brief first using: tm context brief'
|
||||
);
|
||||
}
|
||||
return context.briefId;
|
||||
}
|
||||
|
||||
async getTasks(
|
||||
_projectId?: string,
|
||||
options?: LoadTasksOptions
|
||||
): Promise<Task[]> {
|
||||
const briefId = this.getBriefIdOrThrow();
|
||||
|
||||
// Build query with filters
|
||||
let query = this.supabase
|
||||
.from('tasks')
|
||||
.select(`
|
||||
*,
|
||||
document:document_id (
|
||||
id,
|
||||
document_name,
|
||||
title,
|
||||
description
|
||||
)
|
||||
`)
|
||||
.eq('brief_id', briefId);
|
||||
|
||||
// Apply status filter at database level if specified
|
||||
if (options?.status) {
|
||||
const dbStatus = this.mapStatusToDatabase(options.status);
|
||||
query = query.eq('status', dbStatus);
|
||||
}
|
||||
|
||||
// Apply subtask exclusion at database level if specified
|
||||
if (options?.excludeSubtasks) {
|
||||
// Only fetch parent tasks (where parent_task_id is null)
|
||||
query = query.is('parent_task_id', null);
|
||||
}
|
||||
|
||||
// Execute query with ordering
|
||||
const { data: tasks, error } = await query
|
||||
.order('position', { ascending: true })
|
||||
.order('subtask_position', { ascending: true })
|
||||
.order('created_at', { ascending: true });
|
||||
|
||||
if (error) {
|
||||
throw new Error(`Failed to fetch tasks: ${error.message}`);
|
||||
}
|
||||
|
||||
if (!tasks || tasks.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Type-safe task ID extraction
|
||||
const typedTasks = tasks as TaskWithRelations[];
|
||||
const taskIds = typedTasks.map((t) => t.id);
|
||||
const dependenciesMap =
|
||||
await this.dependencyFetcher.fetchDependenciesWithDisplayIds(taskIds);
|
||||
|
||||
// Use mapper to convert to internal format
|
||||
return TaskMapper.mapDatabaseTasksToTasks(tasks, dependenciesMap);
|
||||
}
|
||||
|
||||
async getTask(_projectId: string, taskId: string): Promise<Task | null> {
|
||||
const briefId = this.getBriefIdOrThrow();
|
||||
|
||||
const { data, error } = await this.supabase
|
||||
.from('tasks')
|
||||
.select('*')
|
||||
.eq('brief_id', briefId)
|
||||
.eq('display_id', taskId.toUpperCase())
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
if (error.code === 'PGRST116') {
|
||||
return null; // Not found
|
||||
}
|
||||
throw new Error(`Failed to fetch task: ${error.message}`);
|
||||
}
|
||||
|
||||
// Get subtasks if this is a parent task
|
||||
const { data: subtasksData } = await this.supabase
|
||||
.from('tasks')
|
||||
.select('*')
|
||||
.eq('parent_task_id', data.id)
|
||||
.order('subtask_position', { ascending: true });
|
||||
|
||||
// Get all task IDs (parent + subtasks) to fetch dependencies
|
||||
const allTaskIds = [data.id, ...(subtasksData?.map((st) => st.id) || [])];
|
||||
|
||||
// Fetch dependencies using the dedicated fetcher
|
||||
const dependenciesByTaskId =
|
||||
await this.dependencyFetcher.fetchDependenciesWithDisplayIds(allTaskIds);
|
||||
|
||||
// Use mapper to convert single task
|
||||
return TaskMapper.mapDatabaseTaskToTask(
|
||||
data,
|
||||
subtasksData || [],
|
||||
dependenciesByTaskId
|
||||
);
|
||||
}
|
||||
|
||||
async updateTask(
|
||||
projectId: string,
|
||||
taskId: string,
|
||||
updates: Partial<Task>
|
||||
): Promise<Task> {
|
||||
const briefId = this.getBriefIdOrThrow();
|
||||
|
||||
// Validate updates using Zod schema
|
||||
try {
|
||||
TaskUpdateSchema.parse(updates);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
const errorMessages = error.issues
|
||||
.map((err) => `${err.path.join('.')}: ${err.message}`)
|
||||
.join(', ');
|
||||
throw new Error(`Invalid task update data: ${errorMessages}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Convert Task fields to database fields with proper typing
|
||||
const dbUpdates: TaskDatabaseUpdate = {};
|
||||
|
||||
if (updates.title !== undefined) dbUpdates.title = updates.title;
|
||||
if (updates.description !== undefined)
|
||||
dbUpdates.description = updates.description;
|
||||
if (updates.status !== undefined)
|
||||
dbUpdates.status = this.mapStatusToDatabase(updates.status);
|
||||
if (updates.priority !== undefined)
|
||||
dbUpdates.priority = this.mapPriorityToDatabase(updates.priority);
|
||||
|
||||
// Handle metadata fields (details, testStrategy, etc.)
|
||||
// Load existing metadata to preserve fields not being updated
|
||||
const { data: existingMetadataRow, error: existingMetadataError } =
|
||||
await this.supabase
|
||||
.from('tasks')
|
||||
.select('metadata')
|
||||
.eq('brief_id', briefId)
|
||||
.eq('display_id', taskId.toUpperCase())
|
||||
.single();
|
||||
|
||||
if (existingMetadataError) {
|
||||
throw new Error(
|
||||
`Failed to load existing task metadata: ${existingMetadataError.message}`
|
||||
);
|
||||
}
|
||||
|
||||
const metadata: Record<string, unknown> = {
|
||||
...((existingMetadataRow?.metadata as Record<string, unknown>) ?? {})
|
||||
};
|
||||
|
||||
if (updates.details !== undefined) metadata.details = updates.details;
|
||||
if (updates.testStrategy !== undefined)
|
||||
metadata.testStrategy = updates.testStrategy;
|
||||
|
||||
if (Object.keys(metadata).length > 0) {
|
||||
dbUpdates.metadata = metadata as Json;
|
||||
}
|
||||
|
||||
// Update the task
|
||||
const { error } = await this.supabase
|
||||
.from('tasks')
|
||||
.update(dbUpdates)
|
||||
.eq('brief_id', briefId)
|
||||
.eq('display_id', taskId.toUpperCase());
|
||||
|
||||
if (error) {
|
||||
throw new Error(`Failed to update task: ${error.message}`);
|
||||
}
|
||||
|
||||
// Return the updated task by fetching it
|
||||
const updatedTask = await this.getTask(projectId, taskId);
|
||||
if (!updatedTask) {
|
||||
throw new Error(`Failed to retrieve updated task ${taskId}`);
|
||||
}
|
||||
|
||||
return updatedTask;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps internal status to database status
|
||||
*/
|
||||
private mapStatusToDatabase(
|
||||
status: string
|
||||
): Database['public']['Enums']['task_status'] {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return 'todo';
|
||||
case 'in-progress':
|
||||
case 'in_progress': // Accept both formats
|
||||
return 'in_progress';
|
||||
case 'done':
|
||||
return 'done';
|
||||
default:
|
||||
throw new Error(
|
||||
`Invalid task status: ${status}. Valid statuses are: pending, in-progress, done`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps internal priority to database priority
|
||||
* Task Master uses 'critical', database uses 'urgent'
|
||||
*/
|
||||
private mapPriorityToDatabase(
|
||||
priority: string
|
||||
): Database['public']['Enums']['task_priority'] {
|
||||
switch (priority) {
|
||||
case 'critical':
|
||||
return 'urgent';
|
||||
case 'low':
|
||||
case 'medium':
|
||||
case 'high':
|
||||
return priority as Database['public']['Enums']['task_priority'];
|
||||
default:
|
||||
throw new Error(
|
||||
`Invalid task priority: ${priority}. Valid priorities are: low, medium, high, critical`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Task, TaskTag } from '../types/index.js';
|
||||
import { LoadTasksOptions } from '../interfaces/storage.interface.js';
|
||||
|
||||
export interface TaskRepository {
|
||||
// Task operations
|
||||
getTasks(projectId: string): Promise<Task[]>;
|
||||
getTasks(projectId: string, options?: LoadTasksOptions): Promise<Task[]>;
|
||||
getTask(projectId: string, taskId: string): Promise<Task | null>;
|
||||
createTask(projectId: string, task: Omit<Task, 'id'>): Promise<Task>;
|
||||
updateTask(
|
||||
|
||||
496
packages/tm-core/src/services/export.service.ts
Normal file
496
packages/tm-core/src/services/export.service.ts
Normal file
@@ -0,0 +1,496 @@
|
||||
/**
|
||||
* @fileoverview Export Service
|
||||
* Core service for exporting tasks to external systems (e.g., Hamster briefs)
|
||||
*/
|
||||
|
||||
import type { Task, TaskStatus } from '../types/index.js';
|
||||
import type { UserContext } from '../auth/types.js';
|
||||
import { ConfigManager } from '../config/config-manager.js';
|
||||
import { AuthManager } from '../auth/auth-manager.js';
|
||||
import { ERROR_CODES, TaskMasterError } from '../errors/task-master-error.js';
|
||||
import { FileStorage } from '../storage/file-storage/index.js';
|
||||
|
||||
// Type definitions for the bulk API response
|
||||
interface TaskImportResult {
|
||||
externalId?: string;
|
||||
index: number;
|
||||
success: boolean;
|
||||
taskId?: string;
|
||||
error?: string;
|
||||
validationErrors?: string[];
|
||||
}
|
||||
|
||||
interface BulkTasksResponse {
|
||||
dryRun: boolean;
|
||||
totalTasks: number;
|
||||
successCount: number;
|
||||
failedCount: number;
|
||||
skippedCount: number;
|
||||
results: TaskImportResult[];
|
||||
summary: {
|
||||
message: string;
|
||||
duration: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for exporting tasks
|
||||
*/
|
||||
export interface ExportTasksOptions {
|
||||
/** Optional tag to export tasks from (uses active tag if not provided) */
|
||||
tag?: string;
|
||||
/** Brief ID to export to */
|
||||
briefId?: string;
|
||||
/** Organization ID (required if briefId is provided) */
|
||||
orgId?: string;
|
||||
/** Filter by task status */
|
||||
status?: TaskStatus;
|
||||
/** Exclude subtasks from export (default: false, subtasks included by default) */
|
||||
excludeSubtasks?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of the export operation
|
||||
*/
|
||||
export interface ExportResult {
|
||||
/** Whether the export was successful */
|
||||
success: boolean;
|
||||
/** Number of tasks exported */
|
||||
taskCount: number;
|
||||
/** The brief ID tasks were exported to */
|
||||
briefId: string;
|
||||
/** The organization ID */
|
||||
orgId: string;
|
||||
/** Optional message */
|
||||
message?: string;
|
||||
/** Error details if export failed */
|
||||
error?: {
|
||||
code: string;
|
||||
message: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Brief information from API
|
||||
*/
|
||||
export interface Brief {
|
||||
id: string;
|
||||
accountId: string;
|
||||
createdAt: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* ExportService handles task export to external systems
|
||||
*/
|
||||
export class ExportService {
|
||||
private configManager: ConfigManager;
|
||||
private authManager: AuthManager;
|
||||
|
||||
constructor(configManager: ConfigManager, authManager: AuthManager) {
|
||||
this.configManager = configManager;
|
||||
this.authManager = authManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export tasks to a brief
|
||||
*/
|
||||
async exportTasks(options: ExportTasksOptions): Promise<ExportResult> {
|
||||
// Validate authentication
|
||||
if (!this.authManager.isAuthenticated()) {
|
||||
throw new TaskMasterError(
|
||||
'Authentication required for export',
|
||||
ERROR_CODES.AUTHENTICATION_ERROR
|
||||
);
|
||||
}
|
||||
|
||||
// Get current context
|
||||
const context = this.authManager.getContext();
|
||||
|
||||
// Determine org and brief IDs
|
||||
let orgId = options.orgId || context?.orgId;
|
||||
let briefId = options.briefId || context?.briefId;
|
||||
|
||||
// Validate we have necessary IDs
|
||||
if (!orgId) {
|
||||
throw new TaskMasterError(
|
||||
'Organization ID is required for export. Use "tm context org" to select one.',
|
||||
ERROR_CODES.MISSING_CONFIGURATION
|
||||
);
|
||||
}
|
||||
|
||||
if (!briefId) {
|
||||
throw new TaskMasterError(
|
||||
'Brief ID is required for export. Use "tm context brief" or provide --brief flag.',
|
||||
ERROR_CODES.MISSING_CONFIGURATION
|
||||
);
|
||||
}
|
||||
|
||||
// Get tasks from the specified or active tag
|
||||
const activeTag = this.configManager.getActiveTag();
|
||||
const tag = options.tag || activeTag;
|
||||
|
||||
// Always read tasks from local file storage for export
|
||||
// (we're exporting local tasks to a remote brief)
|
||||
const fileStorage = new FileStorage(this.configManager.getProjectRoot());
|
||||
await fileStorage.initialize();
|
||||
|
||||
// Load tasks with filters applied at storage layer
|
||||
const filteredTasks = await fileStorage.loadTasks(tag, {
|
||||
status: options.status,
|
||||
excludeSubtasks: options.excludeSubtasks
|
||||
});
|
||||
|
||||
// Get total count (without filters) for comparison
|
||||
const allTasks = await fileStorage.loadTasks(tag);
|
||||
|
||||
const taskListResult = {
|
||||
tasks: filteredTasks,
|
||||
total: allTasks.length,
|
||||
filtered: filteredTasks.length,
|
||||
tag,
|
||||
storageType: 'file' as const
|
||||
};
|
||||
|
||||
if (taskListResult.tasks.length === 0) {
|
||||
return {
|
||||
success: false,
|
||||
taskCount: 0,
|
||||
briefId,
|
||||
orgId,
|
||||
message: 'No tasks found to export',
|
||||
error: {
|
||||
code: 'NO_TASKS',
|
||||
message: 'No tasks match the specified criteria'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// Call the export API with the original tasks
|
||||
// performExport will handle the transformation based on the method used
|
||||
await this.performExport(orgId, briefId, taskListResult.tasks);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
taskCount: taskListResult.tasks.length,
|
||||
briefId,
|
||||
orgId,
|
||||
message: `Successfully exported ${taskListResult.tasks.length} task(s) to brief`
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
taskCount: 0,
|
||||
briefId,
|
||||
orgId,
|
||||
error: {
|
||||
code: 'EXPORT_FAILED',
|
||||
message: errorMessage
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Export tasks from a brief ID or URL
|
||||
*/
|
||||
async exportFromBriefInput(briefInput: string): Promise<ExportResult> {
|
||||
// Extract brief ID from input
|
||||
const briefId = this.extractBriefId(briefInput);
|
||||
if (!briefId) {
|
||||
throw new TaskMasterError(
|
||||
'Invalid brief ID or URL provided',
|
||||
ERROR_CODES.VALIDATION_ERROR
|
||||
);
|
||||
}
|
||||
|
||||
// Fetch brief to get organization
|
||||
const brief = await this.authManager.getBrief(briefId);
|
||||
if (!brief) {
|
||||
throw new TaskMasterError(
|
||||
'Brief not found or you do not have access',
|
||||
ERROR_CODES.NOT_FOUND
|
||||
);
|
||||
}
|
||||
|
||||
// Export with the resolved org and brief
|
||||
return this.exportTasks({
|
||||
orgId: brief.accountId,
|
||||
briefId: brief.id
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate export context before prompting
|
||||
*/
|
||||
async validateContext(): Promise<{
|
||||
hasOrg: boolean;
|
||||
hasBrief: boolean;
|
||||
context: UserContext | null;
|
||||
}> {
|
||||
const context = this.authManager.getContext();
|
||||
|
||||
return {
|
||||
hasOrg: !!context?.orgId,
|
||||
hasBrief: !!context?.briefId,
|
||||
context
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform tasks for API bulk import format (flat structure)
|
||||
*/
|
||||
private transformTasksForBulkImport(tasks: Task[]): any[] {
|
||||
const flatTasks: any[] = [];
|
||||
|
||||
// Process each task and its subtasks
|
||||
tasks.forEach((task) => {
|
||||
// Add parent task
|
||||
flatTasks.push({
|
||||
externalId: String(task.id),
|
||||
title: task.title,
|
||||
description: this.enrichDescription(task),
|
||||
status: this.mapStatusForAPI(task.status),
|
||||
priority: task.priority || 'medium',
|
||||
dependencies: task.dependencies?.map(String) || [],
|
||||
details: task.details,
|
||||
testStrategy: task.testStrategy,
|
||||
complexity: task.complexity,
|
||||
metadata: {
|
||||
complexity: task.complexity,
|
||||
originalId: task.id,
|
||||
originalDescription: task.description,
|
||||
originalDetails: task.details,
|
||||
originalTestStrategy: task.testStrategy
|
||||
}
|
||||
});
|
||||
|
||||
// Add subtasks if they exist
|
||||
if (task.subtasks && task.subtasks.length > 0) {
|
||||
task.subtasks.forEach((subtask) => {
|
||||
flatTasks.push({
|
||||
externalId: `${task.id}.${subtask.id}`,
|
||||
parentExternalId: String(task.id),
|
||||
title: subtask.title,
|
||||
description: this.enrichDescription(subtask),
|
||||
status: this.mapStatusForAPI(subtask.status),
|
||||
priority: subtask.priority || 'medium',
|
||||
dependencies:
|
||||
subtask.dependencies?.map((dep) => {
|
||||
// Convert subtask dependencies to full ID format
|
||||
if (String(dep).includes('.')) {
|
||||
return String(dep);
|
||||
}
|
||||
return `${task.id}.${dep}`;
|
||||
}) || [],
|
||||
details: subtask.details,
|
||||
testStrategy: subtask.testStrategy,
|
||||
complexity: subtask.complexity,
|
||||
metadata: {
|
||||
complexity: subtask.complexity,
|
||||
originalId: subtask.id,
|
||||
originalDescription: subtask.description,
|
||||
originalDetails: subtask.details,
|
||||
originalTestStrategy: subtask.testStrategy
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return flatTasks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enrich task/subtask description with implementation details and test strategy
|
||||
* Creates a comprehensive markdown-formatted description
|
||||
*/
|
||||
private enrichDescription(taskOrSubtask: Task | any): string {
|
||||
const sections: string[] = [];
|
||||
|
||||
// Start with original description if it exists
|
||||
if (taskOrSubtask.description) {
|
||||
sections.push(taskOrSubtask.description);
|
||||
}
|
||||
|
||||
// Add implementation details section
|
||||
if (taskOrSubtask.details) {
|
||||
sections.push('## Implementation Details\n');
|
||||
sections.push(taskOrSubtask.details);
|
||||
}
|
||||
|
||||
// Add test strategy section
|
||||
if (taskOrSubtask.testStrategy) {
|
||||
sections.push('## Test Strategy\n');
|
||||
sections.push(taskOrSubtask.testStrategy);
|
||||
}
|
||||
|
||||
// Join sections with double newlines for better markdown formatting
|
||||
return sections.join('\n\n').trim() || 'No description provided';
|
||||
}
|
||||
|
||||
/**
|
||||
* Map internal status to API status format
|
||||
*/
|
||||
private mapStatusForAPI(status?: string): string {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return 'todo';
|
||||
case 'in-progress':
|
||||
return 'in_progress';
|
||||
case 'done':
|
||||
return 'done';
|
||||
default:
|
||||
return 'todo';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the actual export API call
|
||||
*/
|
||||
private async performExport(
|
||||
orgId: string,
|
||||
briefId: string,
|
||||
tasks: any[]
|
||||
): Promise<void> {
|
||||
// Check if we should use the API endpoint or direct Supabase
|
||||
const useAPIEndpoint = process.env.TM_PUBLIC_BASE_DOMAIN;
|
||||
|
||||
if (useAPIEndpoint) {
|
||||
// Use the new bulk import API endpoint
|
||||
const apiUrl = `${process.env.TM_PUBLIC_BASE_DOMAIN}/ai/api/v1/briefs/${briefId}/tasks/bulk`;
|
||||
|
||||
// Transform tasks to flat structure for API
|
||||
const flatTasks = this.transformTasksForBulkImport(tasks);
|
||||
|
||||
// Prepare request body
|
||||
const requestBody = {
|
||||
source: 'task-master-cli',
|
||||
accountId: orgId,
|
||||
options: {
|
||||
dryRun: false,
|
||||
stopOnError: false
|
||||
},
|
||||
tasks: flatTasks
|
||||
};
|
||||
|
||||
// Get auth token
|
||||
const credentials = this.authManager.getCredentials();
|
||||
if (!credentials || !credentials.token) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
|
||||
// Make API request
|
||||
const response = await fetch(apiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${credentials.token}`
|
||||
},
|
||||
body: JSON.stringify(requestBody)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(
|
||||
`API request failed: ${response.status} - ${errorText}`
|
||||
);
|
||||
}
|
||||
|
||||
const result = (await response.json()) as BulkTasksResponse;
|
||||
|
||||
if (result.failedCount > 0) {
|
||||
const failedTasks = result.results
|
||||
.filter((r) => !r.success)
|
||||
.map((r) => `${r.externalId}: ${r.error}`)
|
||||
.join(', ');
|
||||
console.warn(
|
||||
`Warning: ${result.failedCount} tasks failed to import: ${failedTasks}`
|
||||
);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Successfully exported ${result.successCount} of ${result.totalTasks} tasks to brief ${briefId}`
|
||||
);
|
||||
} else {
|
||||
// Direct Supabase approach is no longer supported
|
||||
// The extractTasks method has been removed from SupabaseTaskRepository
|
||||
// as we now exclusively use the API endpoint for exports
|
||||
throw new Error(
|
||||
'Export API endpoint not configured. Please set TM_PUBLIC_BASE_DOMAIN environment variable to enable task export.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a brief ID from raw input (ID or URL)
|
||||
*/
|
||||
private extractBriefId(input: string): string | null {
|
||||
const raw = input?.trim() ?? '';
|
||||
if (!raw) return null;
|
||||
|
||||
const parseUrl = (s: string): URL | null => {
|
||||
try {
|
||||
return new URL(s);
|
||||
} catch {}
|
||||
try {
|
||||
return new URL(`https://${s}`);
|
||||
} catch {}
|
||||
return null;
|
||||
};
|
||||
|
||||
const fromParts = (path: string): string | null => {
|
||||
const parts = path.split('/').filter(Boolean);
|
||||
const briefsIdx = parts.lastIndexOf('briefs');
|
||||
const candidate =
|
||||
briefsIdx >= 0 && parts.length > briefsIdx + 1
|
||||
? parts[briefsIdx + 1]
|
||||
: parts[parts.length - 1];
|
||||
return candidate?.trim() || null;
|
||||
};
|
||||
|
||||
// Try to parse as URL
|
||||
const url = parseUrl(raw);
|
||||
if (url) {
|
||||
const qId = url.searchParams.get('id') || url.searchParams.get('briefId');
|
||||
const candidate = (qId || fromParts(url.pathname)) ?? null;
|
||||
if (candidate) {
|
||||
if (this.isLikelyId(candidate) || candidate.length >= 8) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if it looks like a path without scheme
|
||||
if (raw.includes('/')) {
|
||||
const candidate = fromParts(raw);
|
||||
if (candidate && (this.isLikelyId(candidate) || candidate.length >= 8)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
// Return as-is if it looks like an ID
|
||||
if (this.isLikelyId(raw) || raw.length >= 8) {
|
||||
return raw;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a string looks like a brief ID (UUID-like)
|
||||
*/
|
||||
private isLikelyId(value: string): boolean {
|
||||
const uuidRegex =
|
||||
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
||||
const ulidRegex = /^[0-9A-HJKMNP-TV-Z]{26}$/i;
|
||||
const slugRegex = /^[A-Za-z0-9_-]{16,}$/;
|
||||
return (
|
||||
uuidRegex.test(value) || ulidRegex.test(value) || slugRegex.test(value)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,4 +5,9 @@
|
||||
|
||||
export { TaskService } from './task-service.js';
|
||||
export { OrganizationService } from './organization.service.js';
|
||||
export { ExportService } from './export.service.js';
|
||||
export type { Organization, Brief } from './organization.service.js';
|
||||
export type {
|
||||
ExportTasksOptions,
|
||||
ExportResult
|
||||
} from './export.service.js';
|
||||
|
||||
@@ -14,6 +14,7 @@ import { ConfigManager } from '../config/config-manager.js';
|
||||
import { StorageFactory } from '../storage/storage-factory.js';
|
||||
import { TaskEntity } from '../entities/task.entity.js';
|
||||
import { ERROR_CODES, TaskMasterError } from '../errors/task-master-error.js';
|
||||
import { getLogger } from '../logger/factory.js';
|
||||
|
||||
/**
|
||||
* Result returned by getTaskList
|
||||
@@ -51,6 +52,7 @@ export class TaskService {
|
||||
private configManager: ConfigManager;
|
||||
private storage: IStorage;
|
||||
private initialized = false;
|
||||
private logger = getLogger('TaskService');
|
||||
|
||||
constructor(configManager: ConfigManager) {
|
||||
this.configManager = configManager;
|
||||
@@ -90,37 +92,76 @@ export class TaskService {
|
||||
const tag = options.tag || activeTag;
|
||||
|
||||
try {
|
||||
// Load raw tasks from storage - storage only knows about tags
|
||||
const rawTasks = await this.storage.loadTasks(tag);
|
||||
// Determine if we can push filters to storage layer
|
||||
const canPushStatusFilter =
|
||||
options.filter?.status &&
|
||||
!options.filter.priority &&
|
||||
!options.filter.tags &&
|
||||
!options.filter.assignee &&
|
||||
!options.filter.search &&
|
||||
options.filter.hasSubtasks === undefined;
|
||||
|
||||
// Build storage-level options
|
||||
const storageOptions: any = {};
|
||||
|
||||
// Push status filter to storage if it's the only filter
|
||||
if (canPushStatusFilter) {
|
||||
const statuses = Array.isArray(options.filter!.status)
|
||||
? options.filter!.status
|
||||
: [options.filter!.status];
|
||||
// Only push single status to storage (multiple statuses need in-memory filtering)
|
||||
if (statuses.length === 1) {
|
||||
storageOptions.status = statuses[0];
|
||||
}
|
||||
}
|
||||
|
||||
// Push subtask exclusion to storage
|
||||
if (options.includeSubtasks === false) {
|
||||
storageOptions.excludeSubtasks = true;
|
||||
}
|
||||
|
||||
// Load tasks from storage with pushed-down filters
|
||||
const rawTasks = await this.storage.loadTasks(tag, storageOptions);
|
||||
|
||||
// Get total count without status filters, but preserve subtask exclusion
|
||||
const baseOptions: any = {};
|
||||
if (options.includeSubtasks === false) {
|
||||
baseOptions.excludeSubtasks = true;
|
||||
}
|
||||
|
||||
const allTasks =
|
||||
storageOptions.status !== undefined
|
||||
? await this.storage.loadTasks(tag, baseOptions)
|
||||
: rawTasks;
|
||||
|
||||
// Convert to TaskEntity for business logic operations
|
||||
const taskEntities = TaskEntity.fromArray(rawTasks);
|
||||
|
||||
// Apply filters if provided
|
||||
// Apply remaining filters in-memory if needed
|
||||
let filteredEntities = taskEntities;
|
||||
if (options.filter) {
|
||||
if (options.filter && !canPushStatusFilter) {
|
||||
filteredEntities = this.applyFilters(taskEntities, options.filter);
|
||||
} else if (
|
||||
options.filter?.status &&
|
||||
Array.isArray(options.filter.status) &&
|
||||
options.filter.status.length > 1
|
||||
) {
|
||||
// Multiple statuses - filter in-memory
|
||||
filteredEntities = this.applyFilters(taskEntities, options.filter);
|
||||
}
|
||||
|
||||
// Convert back to plain objects
|
||||
let tasks = filteredEntities.map((entity) => entity.toJSON());
|
||||
|
||||
// Handle subtasks option
|
||||
if (options.includeSubtasks === false) {
|
||||
tasks = tasks.map((task) => ({
|
||||
...task,
|
||||
subtasks: []
|
||||
}));
|
||||
}
|
||||
const tasks = filteredEntities.map((entity) => entity.toJSON());
|
||||
|
||||
return {
|
||||
tasks,
|
||||
total: rawTasks.length,
|
||||
total: allTasks.length,
|
||||
filtered: filteredEntities.length,
|
||||
tag: tag, // Return the actual tag being used (either explicitly provided or active tag)
|
||||
storageType: this.getStorageType()
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to get task list', error);
|
||||
throw new TaskMasterError(
|
||||
'Failed to get task list',
|
||||
ERROR_CODES.INTERNAL_ERROR,
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
import type {
|
||||
IStorage,
|
||||
StorageStats,
|
||||
UpdateStatusResult
|
||||
UpdateStatusResult,
|
||||
LoadTasksOptions
|
||||
} from '../interfaces/storage.interface.js';
|
||||
import type {
|
||||
Task,
|
||||
@@ -16,7 +17,7 @@ import type {
|
||||
} from '../types/index.js';
|
||||
import { ERROR_CODES, TaskMasterError } from '../errors/task-master-error.js';
|
||||
import { TaskRepository } from '../repositories/task-repository.interface.js';
|
||||
import { SupabaseTaskRepository } from '../repositories/supabase-task-repository.js';
|
||||
import { SupabaseTaskRepository } from '../repositories/supabase/index.js';
|
||||
import { SupabaseClient } from '@supabase/supabase-js';
|
||||
import { AuthManager } from '../auth/auth-manager.js';
|
||||
|
||||
@@ -146,7 +147,7 @@ export class ApiStorage implements IStorage {
|
||||
* Load tasks from API
|
||||
* In our system, the tag parameter represents a brief ID
|
||||
*/
|
||||
async loadTasks(tag?: string): Promise<Task[]> {
|
||||
async loadTasks(tag?: string, options?: LoadTasksOptions): Promise<Task[]> {
|
||||
await this.ensureInitialized();
|
||||
|
||||
try {
|
||||
@@ -160,9 +161,9 @@ export class ApiStorage implements IStorage {
|
||||
);
|
||||
}
|
||||
|
||||
// Load tasks from the current brief context
|
||||
// Load tasks from the current brief context with filters pushed to repository
|
||||
const tasks = await this.retryOperation(() =>
|
||||
this.repository.getTasks(this.projectId)
|
||||
this.repository.getTasks(this.projectId, options)
|
||||
);
|
||||
|
||||
// Update the tag cache with the loaded task IDs
|
||||
|
||||
@@ -6,7 +6,8 @@ import type { Task, TaskMetadata, TaskStatus } from '../../types/index.js';
|
||||
import type {
|
||||
IStorage,
|
||||
StorageStats,
|
||||
UpdateStatusResult
|
||||
UpdateStatusResult,
|
||||
LoadTasksOptions
|
||||
} from '../../interfaces/storage.interface.js';
|
||||
import { FormatHandler } from './format-handler.js';
|
||||
import { FileOperations } from './file-operations.js';
|
||||
@@ -92,15 +93,30 @@ export class FileStorage implements IStorage {
|
||||
* 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, options?: LoadTasksOptions): Promise<Task[]> {
|
||||
const filePath = this.pathResolver.getTasksPath();
|
||||
const resolvedTag = tag || 'master';
|
||||
|
||||
try {
|
||||
const rawData = await this.fileOps.readJson(filePath);
|
||||
const tasks = this.formatHandler.extractTasks(rawData, resolvedTag);
|
||||
let tasks = this.formatHandler.extractTasks(rawData, resolvedTag);
|
||||
|
||||
// Apply filters if provided
|
||||
if (options) {
|
||||
// Filter by status if specified
|
||||
if (options.status) {
|
||||
tasks = tasks.filter((task) => task.status === options.status);
|
||||
}
|
||||
|
||||
// Exclude subtasks if specified
|
||||
if (options.excludeSubtasks) {
|
||||
tasks = tasks.map((task) => ({
|
||||
...task,
|
||||
subtasks: []
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// Enrich tasks with complexity data
|
||||
return await this.enrichTasksWithComplexity(tasks, resolvedTag);
|
||||
} catch (error: any) {
|
||||
if (error.code === 'ENOENT') {
|
||||
|
||||
@@ -14,7 +14,14 @@ import {
|
||||
type StartTaskResult,
|
||||
type ConflictCheckResult
|
||||
} from './services/task-execution-service.js';
|
||||
import {
|
||||
ExportService,
|
||||
type ExportTasksOptions,
|
||||
type ExportResult
|
||||
} from './services/export.service.js';
|
||||
import { AuthManager } from './auth/auth-manager.js';
|
||||
import { ERROR_CODES, TaskMasterError } from './errors/task-master-error.js';
|
||||
import type { UserContext } from './auth/types.js';
|
||||
import type { IConfiguration } from './interfaces/configuration.interface.js';
|
||||
import type {
|
||||
Task,
|
||||
@@ -47,6 +54,10 @@ export type {
|
||||
StartTaskResult,
|
||||
ConflictCheckResult
|
||||
} from './services/task-execution-service.js';
|
||||
export type {
|
||||
ExportTasksOptions,
|
||||
ExportResult
|
||||
} from './services/export.service.js';
|
||||
|
||||
/**
|
||||
* TaskMasterCore facade class
|
||||
@@ -56,6 +67,7 @@ export class TaskMasterCore {
|
||||
private configManager: ConfigManager;
|
||||
private taskService: TaskService;
|
||||
private taskExecutionService: TaskExecutionService;
|
||||
private exportService: ExportService;
|
||||
private executorService: ExecutorService | null = null;
|
||||
|
||||
/**
|
||||
@@ -80,6 +92,7 @@ export class TaskMasterCore {
|
||||
this.configManager = null as any;
|
||||
this.taskService = null as any;
|
||||
this.taskExecutionService = null as any;
|
||||
this.exportService = null as any;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -109,6 +122,10 @@ export class TaskMasterCore {
|
||||
|
||||
// Create task execution service
|
||||
this.taskExecutionService = new TaskExecutionService(this.taskService);
|
||||
|
||||
// Create export service
|
||||
const authManager = AuthManager.getInstance();
|
||||
this.exportService = new ExportService(this.configManager, authManager);
|
||||
} catch (error) {
|
||||
throw new TaskMasterError(
|
||||
'Failed to initialize TaskMasterCore',
|
||||
@@ -242,6 +259,33 @@ export class TaskMasterCore {
|
||||
return this.taskExecutionService.getNextAvailableTask();
|
||||
}
|
||||
|
||||
// ==================== Export Service Methods ====================
|
||||
|
||||
/**
|
||||
* Export tasks to an external system (e.g., Hamster brief)
|
||||
*/
|
||||
async exportTasks(options: ExportTasksOptions): Promise<ExportResult> {
|
||||
return this.exportService.exportTasks(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export tasks from a brief ID or URL
|
||||
*/
|
||||
async exportFromBriefInput(briefInput: string): Promise<ExportResult> {
|
||||
return this.exportService.exportFromBriefInput(briefInput);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate export context before prompting
|
||||
*/
|
||||
async validateExportContext(): Promise<{
|
||||
hasOrg: boolean;
|
||||
hasBrief: boolean;
|
||||
context: UserContext | null;
|
||||
}> {
|
||||
return this.exportService.validateContext();
|
||||
}
|
||||
|
||||
// ==================== Executor Service Methods ====================
|
||||
|
||||
/**
|
||||
|
||||
@@ -82,10 +82,11 @@ export interface Task {
|
||||
}
|
||||
|
||||
/**
|
||||
* Subtask interface extending Task with numeric ID
|
||||
* Subtask interface extending Task
|
||||
* ID can be number (file storage) or string (API storage with display_id)
|
||||
*/
|
||||
export interface Subtask extends Omit<Task, 'id' | 'subtasks'> {
|
||||
id: number;
|
||||
id: number | string;
|
||||
parentId: string;
|
||||
subtasks?: never; // Subtasks cannot have their own subtasks
|
||||
}
|
||||
|
||||
83
packages/tm-core/src/types/repository-types.ts
Normal file
83
packages/tm-core/src/types/repository-types.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Type definitions for repository operations
|
||||
*/
|
||||
import { Database, Tables } from './database.types.js';
|
||||
|
||||
/**
|
||||
* Task row from database with optional joined relations
|
||||
*/
|
||||
export interface TaskWithRelations extends Tables<'tasks'> {
|
||||
document?: {
|
||||
id: string;
|
||||
document_name: string;
|
||||
title: string;
|
||||
description: string | null;
|
||||
} | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dependency row with joined display_id
|
||||
*/
|
||||
export interface DependencyWithDisplayId {
|
||||
task_id: string;
|
||||
depends_on_task: {
|
||||
display_id: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Task metadata structure
|
||||
*/
|
||||
export interface TaskMetadata {
|
||||
details?: string;
|
||||
testStrategy?: string;
|
||||
[key: string]: unknown; // Allow additional fields but be explicit
|
||||
}
|
||||
|
||||
/**
|
||||
* Database update payload for tasks
|
||||
*/
|
||||
export type TaskDatabaseUpdate =
|
||||
Database['public']['Tables']['tasks']['Update'];
|
||||
/**
|
||||
* Configuration for task queries
|
||||
*/
|
||||
export interface TaskQueryConfig {
|
||||
briefId: string;
|
||||
includeSubtasks?: boolean;
|
||||
includeDependencies?: boolean;
|
||||
includeDocument?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of a task fetch operation
|
||||
*/
|
||||
export interface TaskFetchResult {
|
||||
task: Tables<'tasks'>;
|
||||
subtasks: Tables<'tasks'>[];
|
||||
dependencies: Map<string, string[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Task validation errors
|
||||
*/
|
||||
export class TaskValidationError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly field: string,
|
||||
public readonly value: unknown
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'TaskValidationError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Context validation errors
|
||||
*/
|
||||
export class ContextValidationError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'ContextValidationError';
|
||||
}
|
||||
}
|
||||
@@ -12,17 +12,11 @@ import https from 'https';
|
||||
import http from 'http';
|
||||
import inquirer from 'inquirer';
|
||||
import search from '@inquirer/search';
|
||||
import ora from 'ora'; // Import ora
|
||||
|
||||
import { log, readJSON } from './utils.js';
|
||||
// Import new commands from @tm/cli
|
||||
// Import command registry and utilities from @tm/cli
|
||||
import {
|
||||
ListTasksCommand,
|
||||
ShowCommand,
|
||||
AuthCommand,
|
||||
ContextCommand,
|
||||
StartCommand,
|
||||
SetStatusCommand,
|
||||
registerAllCommands,
|
||||
checkForUpdate,
|
||||
performAutoUpdate,
|
||||
displayUpgradeNotification
|
||||
@@ -32,7 +26,6 @@ import {
|
||||
parsePRD,
|
||||
updateTasks,
|
||||
generateTaskFiles,
|
||||
listTasks,
|
||||
expandTask,
|
||||
expandAllTasks,
|
||||
clearSubtasks,
|
||||
@@ -53,11 +46,7 @@ import {
|
||||
validateStrength
|
||||
} from './task-manager.js';
|
||||
|
||||
import {
|
||||
moveTasksBetweenTags,
|
||||
MoveTaskError,
|
||||
MOVE_ERROR_CODES
|
||||
} from './task-manager/move-task.js';
|
||||
import { moveTasksBetweenTags } from './task-manager/move-task.js';
|
||||
|
||||
import {
|
||||
createTag,
|
||||
@@ -72,9 +61,7 @@ import {
|
||||
addDependency,
|
||||
removeDependency,
|
||||
validateDependenciesCommand,
|
||||
fixDependenciesCommand,
|
||||
DependencyError,
|
||||
DEPENDENCY_ERROR_CODES
|
||||
fixDependenciesCommand
|
||||
} from './dependency-manager.js';
|
||||
|
||||
import {
|
||||
@@ -103,7 +90,6 @@ import {
|
||||
displayBanner,
|
||||
displayHelp,
|
||||
displayNextTask,
|
||||
displayTaskById,
|
||||
displayComplexityReport,
|
||||
getStatusWithColor,
|
||||
confirmTaskOverwrite,
|
||||
@@ -112,8 +98,6 @@ import {
|
||||
displayModelConfiguration,
|
||||
displayAvailableModels,
|
||||
displayApiKeyStatus,
|
||||
displayAiUsageSummary,
|
||||
displayMultipleTasksSummary,
|
||||
displayTaggedTasksFYI,
|
||||
displayCurrentTagIndicator,
|
||||
displayCrossTagDependencyError,
|
||||
@@ -137,10 +121,6 @@ import {
|
||||
setModel,
|
||||
getApiKeyStatusReport
|
||||
} from './task-manager/models.js';
|
||||
import {
|
||||
isValidTaskStatus,
|
||||
TASK_STATUS_OPTIONS
|
||||
} from '../../src/constants/task-status.js';
|
||||
import {
|
||||
isValidRulesAction,
|
||||
RULES_ACTIONS,
|
||||
@@ -1687,29 +1667,12 @@ function registerCommands(programInstance) {
|
||||
});
|
||||
});
|
||||
|
||||
// Register the set-status command from @tm/cli
|
||||
// Handles task status updates with proper error handling and validation
|
||||
SetStatusCommand.registerOn(programInstance);
|
||||
|
||||
// NEW: Register the new list command from @tm/cli
|
||||
// This command handles all its own configuration and logic
|
||||
ListTasksCommand.registerOn(programInstance);
|
||||
|
||||
// Register the auth command from @tm/cli
|
||||
// Handles authentication with tryhamster.com
|
||||
AuthCommand.registerOn(programInstance);
|
||||
|
||||
// Register the context command from @tm/cli
|
||||
// Manages workspace context (org/brief selection)
|
||||
ContextCommand.registerOn(programInstance);
|
||||
|
||||
// Register the show command from @tm/cli
|
||||
// Displays detailed information about tasks
|
||||
ShowCommand.registerOn(programInstance);
|
||||
|
||||
// Register the start command from @tm/cli
|
||||
// Starts working on a task by launching claude-code with a standardized prompt
|
||||
StartCommand.registerOn(programInstance);
|
||||
// ========================================
|
||||
// Register All Commands from @tm/cli
|
||||
// ========================================
|
||||
// Use the centralized command registry to register all CLI commands
|
||||
// This replaces individual command registrations and reduces duplication
|
||||
registerAllCommands(programInstance);
|
||||
|
||||
// expand command
|
||||
programInstance
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import path from 'path';
|
||||
|
||||
import { log, readJSON, writeJSON, getCurrentTag } from '../utils.js';
|
||||
import { isTaskDependentOn } from '../task-manager.js';
|
||||
import generateTaskFiles from './generate-task-files.js';
|
||||
|
||||
/**
|
||||
* 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
|
||||
writeJSON(tasksPath, data, projectRoot, tag);
|
||||
|
||||
// Generate task files if requested
|
||||
if (generateFiles) {
|
||||
log('info', 'Regenerating task files...');
|
||||
await generateTaskFiles(tasksPath, path.dirname(tasksPath), context);
|
||||
}
|
||||
// Note: Task file generation is no longer supported and has been removed
|
||||
|
||||
return newSubtask;
|
||||
} catch (error) {
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
setTasksForTag,
|
||||
traverseDependencies
|
||||
} from '../utils.js';
|
||||
import generateTaskFiles from './generate-task-files.js';
|
||||
import {
|
||||
findCrossTagDependencies,
|
||||
getDependentTaskIds,
|
||||
@@ -142,13 +141,7 @@ async function moveTask(
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
// Generate files once at the end if requested
|
||||
if (generateFiles) {
|
||||
await generateTaskFiles(tasksPath, path.dirname(tasksPath), {
|
||||
tag: tag,
|
||||
projectRoot: projectRoot
|
||||
});
|
||||
}
|
||||
// Note: Task file generation is no longer supported and has been removed
|
||||
|
||||
return {
|
||||
message: `Successfully moved ${sourceIds.length} tasks/subtasks`,
|
||||
@@ -209,12 +202,7 @@ async function moveTask(
|
||||
// The writeJSON function will filter out _rawTaggedData automatically
|
||||
writeJSON(tasksPath, rawData, options.projectRoot, tag);
|
||||
|
||||
if (generateFiles) {
|
||||
await generateTaskFiles(tasksPath, path.dirname(tasksPath), {
|
||||
tag: tag,
|
||||
projectRoot: projectRoot
|
||||
});
|
||||
}
|
||||
// Note: Task file generation is no longer supported and has been removed
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import path from 'path';
|
||||
import { log, readJSON, writeJSON } from '../utils.js';
|
||||
import generateTaskFiles from './generate-task-files.js';
|
||||
|
||||
/**
|
||||
* 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
|
||||
writeJSON(tasksPath, data, projectRoot, tag);
|
||||
|
||||
// Generate task files if requested
|
||||
if (generateFiles) {
|
||||
log('info', 'Regenerating task files...');
|
||||
await generateTaskFiles(tasksPath, path.dirname(tasksPath), context);
|
||||
}
|
||||
// Note: Task file generation is no longer supported and has been removed
|
||||
|
||||
return convertedTask;
|
||||
} catch (error) {
|
||||
|
||||
@@ -94,7 +94,6 @@ describe('addSubtask function', () => {
|
||||
const parentTask = writeCallArgs.tasks.find((t) => t.id === 1);
|
||||
expect(parentTask.subtasks).toHaveLength(1);
|
||||
expect(parentTask.subtasks[0].title).toBe('New Subtask');
|
||||
expect(mockGenerateTaskFiles).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should convert an existing task to a subtask', async () => {
|
||||
|
||||
@@ -88,11 +88,6 @@ describe('moveTask (unit)', () => {
|
||||
).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 () => {
|
||||
await expect(
|
||||
moveTask('tasks.json', '1', '2', false, { tag: 'ghost' })
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { defineConfig } from 'tsdown';
|
||||
import { baseConfig, mergeConfig } from '@tm/build-config';
|
||||
import 'dotenv/config';
|
||||
import { config } from 'dotenv';
|
||||
import { resolve } from 'path';
|
||||
|
||||
// Load .env file explicitly with absolute path
|
||||
config({ path: resolve(process.cwd(), '.env') });
|
||||
|
||||
// Get all TM_PUBLIC_* env variables for build-time injection
|
||||
const getBuildTimeEnvs = () => {
|
||||
|
||||
Reference in New Issue
Block a user