Compare commits

..

1 Commits

Author SHA1 Message Date
github-actions[bot]
62c4a25569 docs: auto-update documentation based on changes in next branch
This PR was automatically generated to update documentation based on recent changes.

  Original commit: feat: Add Codex CLI provider with OAuth authentication (#1273)\n\nCo-authored-by: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com>\n\n

  Co-authored-by: Claude <claude-assistant@anthropic.com>
2025-10-05 20:13:58 +00:00
47 changed files with 1531 additions and 2023 deletions

View File

@@ -1,5 +0,0 @@
---
"task-master-ai": patch
---
Fix MCP connection errors caused by deprecated generateTaskFiles calls. Resolves "Cannot read properties of null (reading 'toString')" errors when using MCP tools for task management operations.

View File

@@ -1,5 +0,0 @@
---
"task-master-ai": patch
---
Fix MCP server error when file parameter not provided - now properly constructs default tasks.json path instead of failing with 'tasksJsonPath is required' error.

View File

@@ -1,255 +0,0 @@
/**
* @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;

View File

@@ -493,7 +493,18 @@ export class AuthCommand extends Command {
}
/**
* Register this command on an existing program
* 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
*/
static register(program: Command, name?: string): AuthCommand {
const authCommand = new AuthCommand(name);

View File

@@ -694,7 +694,16 @@ export class ContextCommand extends Command {
}
/**
* Register this command on an existing program
* 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
*/
static register(program: Command, name?: string): ContextCommand {
const contextCommand = new ContextCommand(name);

View File

@@ -1,379 +0,0 @@
/**
* @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;
}
}

View File

@@ -246,7 +246,7 @@ export class ListTasksCommand extends Command {
task.subtasks.forEach((subtask) => {
const subIcon = STATUS_ICONS[subtask.status];
console.log(
` ${chalk.gray(String(subtask.id))} ${subIcon} ${chalk.gray(subtask.title)}`
` ${chalk.gray(`${task.id}.${subtask.id}`)} ${subIcon} ${chalk.gray(subtask.title)}`
);
});
}
@@ -297,7 +297,7 @@ export class ListTasksCommand extends Command {
nextTask
);
// Task table
// Task table - no title, just show the table directly
console.log(
ui.createTaskTable(tasks, {
showSubtasks: withSubtasks,
@@ -474,7 +474,18 @@ export class ListTasksCommand extends Command {
}
/**
* Register this command on an existing program
* 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
*/
static register(program: Command, name?: string): ListTasksCommand {
const listCommand = new ListTasksCommand(name);

View File

@@ -258,6 +258,9 @@ export class SetStatusCommand extends Command {
)
);
}
// Show storage info
console.log(chalk.gray(`\nUsing ${result.storageType} storage`));
}
/**
@@ -287,7 +290,18 @@ export class SetStatusCommand extends Command {
}
/**
* Register this command on an existing program
* Static method to register this command on an existing program
* This is for gradual migration - allows commands.js to use this
*/
static registerOn(program: Command): Command {
const setStatusCommand = new SetStatusCommand();
program.addCommand(setStatusCommand);
return setStatusCommand;
}
/**
* Alternative registration that returns the command for chaining
* Can also configure the command name if needed
*/
static register(program: Command, name?: string): SetStatusCommand {
const setStatusCommand = new SetStatusCommand(name);

View File

@@ -322,7 +322,18 @@ export class ShowCommand extends Command {
}
/**
* Register this command on an existing program
* 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
*/
static register(program: Command, name?: string): ShowCommand {
const showCommand = new ShowCommand(name);

View File

@@ -493,7 +493,16 @@ export class StartCommand extends Command {
}
/**
* Register this command on an existing program
* Static method to register this command on an existing program
*/
static registerOn(program: Command): Command {
const startCommand = new StartCommand();
program.addCommand(startCommand);
return startCommand;
}
/**
* Alternative registration that returns the command for chaining
*/
static register(program: Command, name?: string): StartCommand {
const startCommand = new StartCommand(name);

View File

@@ -10,15 +10,6 @@ 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';

View File

@@ -192,7 +192,8 @@ export function displaySubtasks(
status: any;
description?: string;
dependencies?: string[];
}>
}>,
parentId: string | number
): void {
const terminalWidth = process.stdout.columns * 0.95 || 100;
// Display subtasks header
@@ -227,7 +228,7 @@ export function displaySubtasks(
});
subtasks.forEach((subtask) => {
const subtaskId = String(subtask.id);
const subtaskId = `${parentId}.${subtask.id}`;
// Format dependencies
const deps =
@@ -328,7 +329,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);
displaySubtasks(filteredSubtasks, task.id);
}
}

View File

@@ -286,12 +286,12 @@ export function createTaskTable(
// Adjust column widths to better match the original layout
const baseColWidths = showComplexity
? [
Math.floor(terminalWidth * 0.1),
Math.floor(terminalWidth * 0.06),
Math.floor(terminalWidth * 0.4),
Math.floor(terminalWidth * 0.15),
Math.floor(terminalWidth * 0.1),
Math.floor(terminalWidth * 0.12),
Math.floor(terminalWidth * 0.2),
Math.floor(terminalWidth * 0.1)
Math.floor(terminalWidth * 0.12)
] // ID, Title, Status, Priority, Dependencies, Complexity
: [
Math.floor(terminalWidth * 0.08),
@@ -377,11 +377,7 @@ export function createTaskTable(
}
if (showComplexity) {
const complexityDisplay =
typeof subtask.complexity === 'number'
? getComplexityWithColor(subtask.complexity)
: '--';
subRow.push(chalk.gray(complexityDisplay));
subRow.push(chalk.gray('--'));
}
table.push(subRow);

View File

@@ -75,6 +75,7 @@ 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,3 +318,88 @@ Azure OpenAI provides enterprise-grade OpenAI models through Microsoft's Azure c
- 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

View File

@@ -52,6 +52,18 @@
"capabilities/cli-root-commands",
"capabilities/task-structure"
]
},
{
"group": "AI Providers",
"pages": [
"providers/codex-cli"
]
},
{
"group": "Usage Examples",
"pages": [
"examples/codex-cli-usage"
]
}
]
}

View File

@@ -0,0 +1,466 @@
---
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).

View File

@@ -112,6 +112,19 @@ 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)

View File

@@ -0,0 +1,408 @@
---
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

View File

@@ -69,29 +69,11 @@ export function resolveTasksPath(args, log = silentLogger) {
// Use core findTasksPath with explicit path and normalized projectRoot context
if (projectRoot) {
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;
return coreFindTasksPath(explicitPath, { projectRoot }, log);
}
// Fallback to core function without projectRoot context
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;
return coreFindTasksPath(explicitPath, null, log);
}
/**

67
output.txt Normal file

File diff suppressed because one or more lines are too long

41
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "task-master-ai",
"version": "0.28.0-rc.1",
"version": "0.27.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "task-master-ai",
"version": "0.28.0-rc.1",
"version": "0.27.3",
"license": "MIT WITH Commons-Clause",
"workspaces": [
"apps/*",
@@ -131,7 +131,7 @@
}
},
"apps/extension": {
"version": "0.25.5-rc.0",
"version": "0.25.4",
"dependencies": {
"task-master-ai": "*"
},
@@ -635,6 +635,7 @@
"apps/extension/node_modules/zod": {
"version": "3.25.76",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
@@ -1829,6 +1830,7 @@
"version": "7.28.4",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.3",
@@ -2661,6 +2663,7 @@
"version": "6.3.1",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@dnd-kit/accessibility": "^3.1.1",
"@dnd-kit/utilities": "^3.2.2",
@@ -4580,7 +4583,6 @@
"version": "0.23.2",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
}
@@ -5170,7 +5172,6 @@
"version": "0.23.2",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
}
@@ -5179,6 +5180,7 @@
"version": "3.25.76",
"dev": true,
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
@@ -5469,6 +5471,7 @@
"node_modules/@modelcontextprotocol/sdk/node_modules/zod": {
"version": "3.25.76",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
@@ -5569,6 +5572,7 @@
"node_modules/@opentelemetry/api": {
"version": "1.9.0",
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=8.0.0"
}
@@ -8588,6 +8592,7 @@
"version": "19.1.8",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@@ -8596,6 +8601,7 @@
"version": "19.1.6",
"dev": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^19.0.0"
}
@@ -9041,6 +9047,7 @@
"node_modules/acorn": {
"version": "8.15.0",
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -9106,6 +9113,7 @@
"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",
@@ -9325,6 +9333,7 @@
"node_modules/ajv": {
"version": "8.17.1",
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -10330,6 +10339,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.3",
"caniuse-lite": "^1.0.30001741",
@@ -12193,7 +12203,8 @@
"node_modules/devtools-protocol": {
"version": "0.0.1312386",
"dev": true,
"license": "BSD-3-Clause"
"license": "BSD-3-Clause",
"peer": true
},
"node_modules/dezalgo": {
"version": "1.0.4",
@@ -12787,6 +12798,7 @@
"version": "0.25.10",
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"bin": {
"esbuild": "bin/esbuild"
},
@@ -13099,6 +13111,7 @@
"node_modules/express": {
"version": "4.21.2",
"license": "MIT",
"peer": true,
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
@@ -15452,6 +15465,7 @@
"version": "6.3.1",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@alcalzone/ansi-tokenize": "^0.2.0",
"ansi-escapes": "^7.0.0",
@@ -16409,6 +16423,7 @@
"version": "29.7.0",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@jest/core": "^29.7.0",
"@jest/types": "^29.6.3",
@@ -18026,6 +18041,7 @@
"version": "1.4.0",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 10.16.0"
}
@@ -18351,7 +18367,6 @@
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
@@ -18576,7 +18591,6 @@
"version": "1.4.0",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
@@ -18707,6 +18721,7 @@
"node_modules/marked": {
"version": "15.0.12",
"license": "MIT",
"peer": true,
"bin": {
"marked": "bin/marked.js"
},
@@ -21429,6 +21444,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -22811,6 +22827,7 @@
"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",
@@ -25239,6 +25256,7 @@
"version": "5.9.2",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -25355,6 +25373,7 @@
"version": "11.0.5",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/unist": "^3.0.0",
"bail": "^2.0.0",
@@ -25797,6 +25816,7 @@
"version": "5.4.20",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",
@@ -25909,7 +25929,6 @@
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": ">=12"
}
@@ -26493,7 +26512,7 @@
},
"node_modules/yaml": {
"version": "1.10.2",
"devOptional": true,
"dev": true,
"license": "ISC",
"engines": {
"node": ">= 6"
@@ -26636,6 +26655,7 @@
"node_modules/zod": {
"version": "4.1.11",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
@@ -27377,6 +27397,7 @@
"version": "3.2.4",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/chai": "^5.2.2",
"@vitest/expect": "3.2.4",

View File

@@ -53,7 +53,7 @@ export class TaskEntity implements Task {
// Normalize subtask IDs to strings
this.subtasks = (data.subtasks || []).map((subtask) => ({
...subtask,
id: String(subtask.id),
id: Number(subtask.id), // Keep subtask IDs as numbers per interface
parentId: String(subtask.parentId)
}));

View File

@@ -51,8 +51,7 @@ export const ERROR_CODES = {
INTERNAL_ERROR: 'INTERNAL_ERROR',
INVALID_INPUT: 'INVALID_INPUT',
NOT_IMPLEMENTED: 'NOT_IMPLEMENTED',
UNKNOWN_ERROR: 'UNKNOWN_ERROR',
NOT_FOUND: 'NOT_FOUND'
UNKNOWN_ERROR: 'UNKNOWN_ERROR'
} as const;
export type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES];

View File

@@ -11,9 +11,7 @@ export {
type ListTasksResult,
type StartTaskOptions,
type StartTaskResult,
type ConflictCheckResult,
type ExportTasksOptions,
type ExportResult
type ConflictCheckResult
} from './task-master-core.js';
// Re-export types

View File

@@ -5,16 +5,6 @@
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
*/
@@ -31,12 +21,11 @@ export interface UpdateStatusResult {
*/
export interface IStorage {
/**
* Load all tasks from storage, optionally filtered by tag and other criteria
* Load all tasks from storage, optionally filtered by tag
* @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, options?: LoadTasksOptions): Promise<Task[]>;
loadTasks(tag?: string): Promise<Task[]>;
/**
* Load a single task by ID
@@ -216,7 +205,7 @@ export abstract class BaseStorage implements IStorage {
}
// Abstract methods that must be implemented by concrete classes
abstract loadTasks(tag?: string, options?: LoadTasksOptions): Promise<Task[]>;
abstract loadTasks(tag?: string): 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>;

View File

@@ -1,148 +0,0 @@
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');
});
});
});

View File

@@ -2,32 +2,22 @@ import { Task, Subtask } from '../types/index.js';
import { Database, Tables } from '../types/database.types.js';
type TaskRow = Tables<'tasks'>;
// Legacy type for backward compatibility
type DependencyRow = Tables<'task_dependencies'> & {
depends_on_task?: { display_id: string } | null;
depends_on_task_id?: string;
};
type DependencyRow = Tables<'task_dependencies'>;
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[],
dependencies: Map<string, string[]> | DependencyRow[]
dbDependencies: DependencyRow[]
): Task[] {
if (!dbTasks || dbTasks.length === 0) {
return [];
}
// Handle both Map and array formats for backward compatibility
const dependenciesByTaskId =
dependencies instanceof Map
? dependencies
: this.groupDependenciesByTaskId(dependencies);
// Group dependencies by task_id
const dependenciesByTaskId = this.groupDependenciesByTaskId(dbDependencies);
// Separate parent tasks and subtasks
const parentTasks = dbTasks.filter((t) => !t.parent_task_id);
@@ -53,23 +43,21 @@ export class TaskMapper {
): Task {
// Map subtasks
const subtasks: Subtask[] = dbSubtasks.map((subtask, index) => ({
id: subtask.display_id || String(index + 1), // Use display_id if available (API storage), fallback to numeric (file storage)
id: index + 1, // Use numeric ID for subtasks
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: this.extractMetadataField(subtask.metadata, 'details', ''),
testStrategy: this.extractMetadataField(
subtask.metadata,
'testStrategy',
''
),
details: (subtask.metadata as any)?.details || '',
testStrategy: (subtask.metadata as any)?.testStrategy || '',
createdAt: subtask.created_at,
updatedAt: subtask.updated_at,
assignee: subtask.assignee_id || undefined,
complexity: subtask.complexity ?? undefined
complexity: subtask.complexity
? this.mapComplexityToInternal(subtask.complexity)
: undefined
}));
return {
@@ -79,25 +67,22 @@ export class TaskMapper {
status: this.mapStatus(dbTask.status),
priority: this.mapPriority(dbTask.priority),
dependencies: dependenciesByTaskId.get(dbTask.id) || [],
details: this.extractMetadataField(dbTask.metadata, 'details', ''),
testStrategy: this.extractMetadataField(
dbTask.metadata,
'testStrategy',
''
),
details: (dbTask.metadata as any)?.details || '',
testStrategy: (dbTask.metadata as any)?.testStrategy || '',
subtasks,
createdAt: dbTask.created_at,
updatedAt: dbTask.updated_at,
assignee: dbTask.assignee_id || undefined,
complexity: dbTask.complexity ?? undefined,
complexity: dbTask.complexity
? this.mapComplexityToInternal(dbTask.complexity)
: undefined,
effort: dbTask.estimated_hours || undefined,
actualEffort: dbTask.actual_hours || undefined
};
}
/**
* Groups dependencies by task ID (legacy method for backward compatibility)
* @deprecated Use DependencyFetcher.fetchDependenciesWithDisplayIds instead
* Groups dependencies by task ID
*/
private static groupDependenciesByTaskId(
dependencies: DependencyRow[]
@@ -107,14 +92,7 @@ export class TaskMapper {
if (dependencies) {
for (const dep of dependencies) {
const deps = dependenciesByTaskId.get(dep.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);
}
deps.push(dep.depends_on_task_id);
dependenciesByTaskId.set(dep.task_id, deps);
}
}
@@ -179,38 +157,14 @@ export class TaskMapper {
}
/**
* 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
* Maps numeric complexity to descriptive complexity
*/
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;
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';
}
}

View File

@@ -0,0 +1,224 @@
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`
);
}
}
}

View File

@@ -1,68 +0,0 @@
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;
}
}

View File

@@ -1,5 +0,0 @@
/**
* Supabase repository implementations
*/
export { SupabaseTaskRepository } from './supabase-task-repository.js';
export { DependencyFetcher } from './dependency-fetcher.js';

View File

@@ -1,275 +0,0 @@
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`
);
}
}
}

View File

@@ -1,9 +1,8 @@
import { Task, TaskTag } from '../types/index.js';
import { LoadTasksOptions } from '../interfaces/storage.interface.js';
export interface TaskRepository {
// Task operations
getTasks(projectId: string, options?: LoadTasksOptions): Promise<Task[]>;
getTasks(projectId: string): Promise<Task[]>;
getTask(projectId: string, taskId: string): Promise<Task | null>;
createTask(projectId: string, task: Omit<Task, 'id'>): Promise<Task>;
updateTask(

View File

@@ -1,496 +0,0 @@
/**
* @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)
);
}
}

View File

@@ -5,9 +5,4 @@
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';

View File

@@ -14,7 +14,6 @@ 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
@@ -52,7 +51,6 @@ export class TaskService {
private configManager: ConfigManager;
private storage: IStorage;
private initialized = false;
private logger = getLogger('TaskService');
constructor(configManager: ConfigManager) {
this.configManager = configManager;
@@ -92,76 +90,37 @@ export class TaskService {
const tag = options.tag || activeTag;
try {
// 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;
// Load raw tasks from storage - storage only knows about tags
const rawTasks = await this.storage.loadTasks(tag);
// Convert to TaskEntity for business logic operations
const taskEntities = TaskEntity.fromArray(rawTasks);
// Apply remaining filters in-memory if needed
// Apply filters if provided
let filteredEntities = taskEntities;
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
if (options.filter) {
filteredEntities = this.applyFilters(taskEntities, options.filter);
}
// Convert back to plain objects
const tasks = filteredEntities.map((entity) => entity.toJSON());
let tasks = filteredEntities.map((entity) => entity.toJSON());
// Handle subtasks option
if (options.includeSubtasks === false) {
tasks = tasks.map((task) => ({
...task,
subtasks: []
}));
}
return {
tasks,
total: allTasks.length,
total: rawTasks.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,

View File

@@ -6,8 +6,7 @@
import type {
IStorage,
StorageStats,
UpdateStatusResult,
LoadTasksOptions
UpdateStatusResult
} from '../interfaces/storage.interface.js';
import type {
Task,
@@ -17,7 +16,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/index.js';
import { SupabaseTaskRepository } from '../repositories/supabase-task-repository.js';
import { SupabaseClient } from '@supabase/supabase-js';
import { AuthManager } from '../auth/auth-manager.js';
@@ -147,7 +146,7 @@ export class ApiStorage implements IStorage {
* Load tasks from API
* In our system, the tag parameter represents a brief ID
*/
async loadTasks(tag?: string, options?: LoadTasksOptions): Promise<Task[]> {
async loadTasks(tag?: string): Promise<Task[]> {
await this.ensureInitialized();
try {
@@ -161,9 +160,9 @@ export class ApiStorage implements IStorage {
);
}
// Load tasks from the current brief context with filters pushed to repository
// Load tasks from the current brief context
const tasks = await this.retryOperation(() =>
this.repository.getTasks(this.projectId, options)
this.repository.getTasks(this.projectId)
);
// Update the tag cache with the loaded task IDs

View File

@@ -6,8 +6,7 @@ import type { Task, TaskMetadata, TaskStatus } from '../../types/index.js';
import type {
IStorage,
StorageStats,
UpdateStatusResult,
LoadTasksOptions
UpdateStatusResult
} from '../../interfaces/storage.interface.js';
import { FormatHandler } from './format-handler.js';
import { FileOperations } from './file-operations.js';
@@ -93,30 +92,15 @@ 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, options?: LoadTasksOptions): Promise<Task[]> {
async loadTasks(tag?: string): Promise<Task[]> {
const filePath = this.pathResolver.getTasksPath();
const resolvedTag = tag || 'master';
try {
const rawData = await this.fileOps.readJson(filePath);
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: []
}));
}
}
const tasks = this.formatHandler.extractTasks(rawData, resolvedTag);
// Enrich tasks with complexity data
return await this.enrichTasksWithComplexity(tasks, resolvedTag);
} catch (error: any) {
if (error.code === 'ENOENT') {

View File

@@ -14,14 +14,7 @@ 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,
@@ -54,10 +47,6 @@ export type {
StartTaskResult,
ConflictCheckResult
} from './services/task-execution-service.js';
export type {
ExportTasksOptions,
ExportResult
} from './services/export.service.js';
/**
* TaskMasterCore facade class
@@ -67,7 +56,6 @@ export class TaskMasterCore {
private configManager: ConfigManager;
private taskService: TaskService;
private taskExecutionService: TaskExecutionService;
private exportService: ExportService;
private executorService: ExecutorService | null = null;
/**
@@ -92,7 +80,6 @@ export class TaskMasterCore {
this.configManager = null as any;
this.taskService = null as any;
this.taskExecutionService = null as any;
this.exportService = null as any;
}
/**
@@ -122,10 +109,6 @@ 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',
@@ -259,33 +242,6 @@ 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 ====================
/**

View File

@@ -82,11 +82,10 @@ export interface Task {
}
/**
* Subtask interface extending Task
* ID can be number (file storage) or string (API storage with display_id)
* Subtask interface extending Task with numeric ID
*/
export interface Subtask extends Omit<Task, 'id' | 'subtasks'> {
id: number | string;
id: number;
parentId: string;
subtasks?: never; // Subtasks cannot have their own subtasks
}

View File

@@ -1,83 +0,0 @@
/**
* 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';
}
}

View File

@@ -12,11 +12,17 @@ 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 command registry and utilities from @tm/cli
// Import new commands from @tm/cli
import {
registerAllCommands,
ListTasksCommand,
ShowCommand,
AuthCommand,
ContextCommand,
StartCommand,
SetStatusCommand,
checkForUpdate,
performAutoUpdate,
displayUpgradeNotification
@@ -26,6 +32,7 @@ import {
parsePRD,
updateTasks,
generateTaskFiles,
listTasks,
expandTask,
expandAllTasks,
clearSubtasks,
@@ -46,7 +53,11 @@ import {
validateStrength
} from './task-manager.js';
import { moveTasksBetweenTags } from './task-manager/move-task.js';
import {
moveTasksBetweenTags,
MoveTaskError,
MOVE_ERROR_CODES
} from './task-manager/move-task.js';
import {
createTag,
@@ -61,7 +72,9 @@ import {
addDependency,
removeDependency,
validateDependenciesCommand,
fixDependenciesCommand
fixDependenciesCommand,
DependencyError,
DEPENDENCY_ERROR_CODES
} from './dependency-manager.js';
import {
@@ -90,6 +103,7 @@ import {
displayBanner,
displayHelp,
displayNextTask,
displayTaskById,
displayComplexityReport,
getStatusWithColor,
confirmTaskOverwrite,
@@ -98,6 +112,8 @@ import {
displayModelConfiguration,
displayAvailableModels,
displayApiKeyStatus,
displayAiUsageSummary,
displayMultipleTasksSummary,
displayTaggedTasksFYI,
displayCurrentTagIndicator,
displayCrossTagDependencyError,
@@ -121,6 +137,10 @@ import {
setModel,
getApiKeyStatusReport
} from './task-manager/models.js';
import {
isValidTaskStatus,
TASK_STATUS_OPTIONS
} from '../../src/constants/task-status.js';
import {
isValidRulesAction,
RULES_ACTIONS,
@@ -1667,12 +1687,29 @@ function registerCommands(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);
// 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);
// expand command
programInstance

View File

@@ -1,5 +1,8 @@
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
@@ -139,7 +142,11 @@ async function addSubtask(
// Write the updated tasks back to the file with proper context
writeJSON(tasksPath, data, projectRoot, tag);
// Note: Task file generation is no longer supported and has been removed
// Generate task files if requested
if (generateFiles) {
log('info', 'Regenerating task files...');
await generateTaskFiles(tasksPath, path.dirname(tasksPath), context);
}
return newSubtask;
} catch (error) {

View File

@@ -6,6 +6,7 @@ import {
setTasksForTag,
traverseDependencies
} from '../utils.js';
import generateTaskFiles from './generate-task-files.js';
import {
findCrossTagDependencies,
getDependentTaskIds,
@@ -141,7 +142,13 @@ async function moveTask(
results.push(result);
}
// Note: Task file generation is no longer supported and has been removed
// Generate files once at the end if requested
if (generateFiles) {
await generateTaskFiles(tasksPath, path.dirname(tasksPath), {
tag: tag,
projectRoot: projectRoot
});
}
return {
message: `Successfully moved ${sourceIds.length} tasks/subtasks`,
@@ -202,7 +209,12 @@ async function moveTask(
// The writeJSON function will filter out _rawTaggedData automatically
writeJSON(tasksPath, rawData, options.projectRoot, tag);
// Note: Task file generation is no longer supported and has been removed
if (generateFiles) {
await generateTaskFiles(tasksPath, path.dirname(tasksPath), {
tag: tag,
projectRoot: projectRoot
});
}
return result;
}

View File

@@ -1,4 +1,6 @@
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
@@ -106,7 +108,11 @@ async function removeSubtask(
// Write the updated tasks back to the file with proper context
writeJSON(tasksPath, data, projectRoot, tag);
// Note: Task file generation is no longer supported and has been removed
// Generate task files if requested
if (generateFiles) {
log('info', 'Regenerating task files...');
await generateTaskFiles(tasksPath, path.dirname(tasksPath), context);
}
return convertedTask;
} catch (error) {

View File

@@ -94,6 +94,7 @@ 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 () => {

View File

@@ -88,6 +88,11 @@ 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' })

View File

@@ -1,10 +1,6 @@
import { defineConfig } from 'tsdown';
import { baseConfig, mergeConfig } from '@tm/build-config';
import { config } from 'dotenv';
import { resolve } from 'path';
// Load .env file explicitly with absolute path
config({ path: resolve(process.cwd(), '.env') });
import 'dotenv/config';
// Get all TM_PUBLIC_* env variables for build-time injection
const getBuildTimeEnvs = () => {