refactor(tm-core): migrate to Vitest and Biome, implement clean architecture
- Migrated from Jest to Vitest for faster test execution (~4.2s vs ~4.6-5s) - Replaced ESLint and Prettier with Biome for unified, faster linting/formatting - Implemented BaseProvider with Template Method pattern following clean code principles - Created TaskEntity with business logic encapsulation - Added TaskMasterCore facade as main entry point - Implemented complete end-to-end listTasks functionality - All 50 tests passing with improved performance 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,6 @@
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import { TaskPriority, TaskComplexity } from '../types/index.js';
|
||||
|
||||
// ============================================================================
|
||||
// Enum Schemas
|
||||
@@ -17,12 +16,7 @@ export const taskPrioritySchema = z.enum(['low', 'medium', 'high', 'critical']);
|
||||
/**
|
||||
* Task complexity validation schema
|
||||
*/
|
||||
export const taskComplexitySchema = z.enum([
|
||||
'simple',
|
||||
'moderate',
|
||||
'complex',
|
||||
'very-complex'
|
||||
]);
|
||||
export const taskComplexitySchema = z.enum(['simple', 'moderate', 'complex', 'very-complex']);
|
||||
|
||||
/**
|
||||
* Log level validation schema
|
||||
@@ -37,11 +31,7 @@ export const storageTypeSchema = z.enum(['file', 'memory', 'database']);
|
||||
/**
|
||||
* Tag naming convention validation schema
|
||||
*/
|
||||
export const tagNamingConventionSchema = z.enum([
|
||||
'kebab-case',
|
||||
'camelCase',
|
||||
'snake_case'
|
||||
]);
|
||||
export const tagNamingConventionSchema = z.enum(['kebab-case', 'camelCase', 'snake_case']);
|
||||
|
||||
/**
|
||||
* Buffer encoding validation schema
|
||||
@@ -233,6 +223,4 @@ export const cacheConfigSchema = z
|
||||
// ============================================================================
|
||||
|
||||
export type ConfigurationSchema = z.infer<typeof configurationSchema>;
|
||||
export type PartialConfigurationSchema = z.infer<
|
||||
typeof partialConfigurationSchema
|
||||
>;
|
||||
export type PartialConfigurationSchema = z.infer<typeof partialConfigurationSchema>;
|
||||
|
||||
239
packages/tm-core/src/core/entities/task.entity.ts
Normal file
239
packages/tm-core/src/core/entities/task.entity.ts
Normal file
@@ -0,0 +1,239 @@
|
||||
/**
|
||||
* @fileoverview Task entity with business rules and domain logic
|
||||
*/
|
||||
|
||||
import { ERROR_CODES, TaskMasterError } from '../../errors/task-master-error.js';
|
||||
import type { Subtask, Task, TaskPriority, TaskStatus } from '../../types/index.js';
|
||||
|
||||
/**
|
||||
* Task entity representing a task with business logic
|
||||
* Encapsulates validation and state management rules
|
||||
*/
|
||||
export class TaskEntity implements Task {
|
||||
readonly id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
status: TaskStatus;
|
||||
priority: TaskPriority;
|
||||
dependencies: string[];
|
||||
details: string;
|
||||
testStrategy: string;
|
||||
subtasks: Subtask[];
|
||||
|
||||
// Optional properties
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
effort?: number;
|
||||
actualEffort?: number;
|
||||
tags?: string[];
|
||||
assignee?: string;
|
||||
complexity?: Task['complexity'];
|
||||
|
||||
constructor(data: Task) {
|
||||
this.validate(data);
|
||||
|
||||
this.id = data.id;
|
||||
this.title = data.title;
|
||||
this.description = data.description;
|
||||
this.status = data.status;
|
||||
this.priority = data.priority;
|
||||
this.dependencies = data.dependencies || [];
|
||||
this.details = data.details;
|
||||
this.testStrategy = data.testStrategy;
|
||||
this.subtasks = data.subtasks || [];
|
||||
|
||||
// Optional properties
|
||||
this.createdAt = data.createdAt;
|
||||
this.updatedAt = data.updatedAt;
|
||||
this.effort = data.effort;
|
||||
this.actualEffort = data.actualEffort;
|
||||
this.tags = data.tags;
|
||||
this.assignee = data.assignee;
|
||||
this.complexity = data.complexity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate task data
|
||||
*/
|
||||
private validate(data: Partial<Task>): void {
|
||||
if (!data.id || typeof data.id !== 'string') {
|
||||
throw new TaskMasterError(
|
||||
'Task ID is required and must be a string',
|
||||
ERROR_CODES.VALIDATION_ERROR
|
||||
);
|
||||
}
|
||||
|
||||
if (!data.title || data.title.trim().length === 0) {
|
||||
throw new TaskMasterError('Task title is required', ERROR_CODES.VALIDATION_ERROR);
|
||||
}
|
||||
|
||||
if (!data.description || data.description.trim().length === 0) {
|
||||
throw new TaskMasterError('Task description is required', ERROR_CODES.VALIDATION_ERROR);
|
||||
}
|
||||
|
||||
if (!this.isValidStatus(data.status)) {
|
||||
throw new TaskMasterError(
|
||||
`Invalid task status: ${data.status}`,
|
||||
ERROR_CODES.VALIDATION_ERROR
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.isValidPriority(data.priority)) {
|
||||
throw new TaskMasterError(
|
||||
`Invalid task priority: ${data.priority}`,
|
||||
ERROR_CODES.VALIDATION_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if status is valid
|
||||
*/
|
||||
private isValidStatus(status: any): status is TaskStatus {
|
||||
return [
|
||||
'pending',
|
||||
'in-progress',
|
||||
'done',
|
||||
'deferred',
|
||||
'cancelled',
|
||||
'blocked',
|
||||
'review'
|
||||
].includes(status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if priority is valid
|
||||
*/
|
||||
private isValidPriority(priority: any): priority is TaskPriority {
|
||||
return ['low', 'medium', 'high', 'critical'].includes(priority);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if task can be marked as complete
|
||||
*/
|
||||
canComplete(): boolean {
|
||||
// Cannot complete if status is already done or cancelled
|
||||
if (this.status === 'done' || this.status === 'cancelled') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cannot complete if blocked
|
||||
if (this.status === 'blocked') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if all subtasks are complete
|
||||
const allSubtasksComplete = this.subtasks.every(
|
||||
(subtask) => subtask.status === 'done' || subtask.status === 'cancelled'
|
||||
);
|
||||
|
||||
return allSubtasksComplete;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark task as complete
|
||||
*/
|
||||
markAsComplete(): void {
|
||||
if (!this.canComplete()) {
|
||||
throw new TaskMasterError(
|
||||
'Task cannot be marked as complete',
|
||||
ERROR_CODES.TASK_STATUS_ERROR,
|
||||
{
|
||||
taskId: this.id,
|
||||
currentStatus: this.status,
|
||||
hasIncompleteSubtasks: this.subtasks.some(
|
||||
(s) => s.status !== 'done' && s.status !== 'cancelled'
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
this.status = 'done';
|
||||
this.updatedAt = new Date().toISOString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if task has dependencies
|
||||
*/
|
||||
hasDependencies(): boolean {
|
||||
return this.dependencies.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if task has subtasks
|
||||
*/
|
||||
hasSubtasks(): boolean {
|
||||
return this.subtasks.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a subtask
|
||||
*/
|
||||
addSubtask(subtask: Omit<Subtask, 'id' | 'parentId'>): void {
|
||||
const nextId = this.subtasks.length + 1;
|
||||
this.subtasks.push({
|
||||
...subtask,
|
||||
id: nextId,
|
||||
parentId: this.id
|
||||
});
|
||||
this.updatedAt = new Date().toISOString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update task status
|
||||
*/
|
||||
updateStatus(newStatus: TaskStatus): void {
|
||||
if (!this.isValidStatus(newStatus)) {
|
||||
throw new TaskMasterError(`Invalid status: ${newStatus}`, ERROR_CODES.VALIDATION_ERROR);
|
||||
}
|
||||
|
||||
// Business rule: Cannot move from done to pending
|
||||
if (this.status === 'done' && newStatus === 'pending') {
|
||||
throw new TaskMasterError(
|
||||
'Cannot move completed task back to pending',
|
||||
ERROR_CODES.TASK_STATUS_ERROR
|
||||
);
|
||||
}
|
||||
|
||||
this.status = newStatus;
|
||||
this.updatedAt = new Date().toISOString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert entity to plain object
|
||||
*/
|
||||
toJSON(): Task {
|
||||
return {
|
||||
id: this.id,
|
||||
title: this.title,
|
||||
description: this.description,
|
||||
status: this.status,
|
||||
priority: this.priority,
|
||||
dependencies: this.dependencies,
|
||||
details: this.details,
|
||||
testStrategy: this.testStrategy,
|
||||
subtasks: this.subtasks,
|
||||
createdAt: this.createdAt,
|
||||
updatedAt: this.updatedAt,
|
||||
effort: this.effort,
|
||||
actualEffort: this.actualEffort,
|
||||
tags: this.tags,
|
||||
assignee: this.assignee,
|
||||
complexity: this.complexity
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create TaskEntity from plain object
|
||||
*/
|
||||
static fromObject(data: Task): TaskEntity {
|
||||
return new TaskEntity(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create multiple TaskEntities from array
|
||||
*/
|
||||
static fromArray(data: Task[]): TaskEntity[] {
|
||||
return data.map((task) => new TaskEntity(task));
|
||||
}
|
||||
}
|
||||
@@ -174,8 +174,8 @@ export class TaskMasterError extends Error {
|
||||
}
|
||||
|
||||
// If we have a cause error, append its stack trace
|
||||
if (cause && cause.stack) {
|
||||
this.stack = this.stack + '\nCaused by: ' + cause.stack;
|
||||
if (cause?.stack) {
|
||||
this.stack = `${this.stack}\nCaused by: ${cause.stack}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,14 +211,7 @@ export class TaskMasterError extends Error {
|
||||
private containsSensitiveInfo(obj: any): boolean {
|
||||
if (typeof obj !== 'object' || obj === null) return false;
|
||||
|
||||
const sensitiveKeys = [
|
||||
'password',
|
||||
'token',
|
||||
'key',
|
||||
'secret',
|
||||
'auth',
|
||||
'credential'
|
||||
];
|
||||
const sensitiveKeys = ['password', 'token', 'key', 'secret', 'auth', 'credential'];
|
||||
const objString = JSON.stringify(obj).toLowerCase();
|
||||
|
||||
return sensitiveKeys.some((key) => objString.includes(key));
|
||||
@@ -300,9 +293,7 @@ export class TaskMasterError extends Error {
|
||||
/**
|
||||
* Create a new error with additional context
|
||||
*/
|
||||
public withContext(
|
||||
additionalContext: Partial<ErrorContext>
|
||||
): TaskMasterError {
|
||||
public withContext(additionalContext: Partial<ErrorContext>): TaskMasterError {
|
||||
return new TaskMasterError(
|
||||
this.message,
|
||||
this.code,
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
* This file exports all public APIs from the core Task Master library
|
||||
*/
|
||||
|
||||
// Export main facade
|
||||
export {
|
||||
TaskMasterCore,
|
||||
createTaskMasterCore,
|
||||
type TaskMasterCoreOptions,
|
||||
type ListTasksResult
|
||||
} from './task-master-core.js';
|
||||
|
||||
// Re-export types
|
||||
export type * from './types/index';
|
||||
|
||||
@@ -25,6 +33,9 @@ export * from './utils/index';
|
||||
// Re-export errors
|
||||
export * from './errors/index';
|
||||
|
||||
// Re-export entities
|
||||
export { TaskEntity } from './core/entities/task.entity.js';
|
||||
|
||||
// Package metadata
|
||||
export const version = '1.0.0';
|
||||
export const name = '@task-master/tm-core';
|
||||
|
||||
@@ -282,10 +282,7 @@ export abstract class BaseAIProvider implements IAIProvider {
|
||||
}
|
||||
|
||||
// Abstract methods that must be implemented by concrete classes
|
||||
abstract generateCompletion(
|
||||
prompt: string,
|
||||
options?: AIOptions
|
||||
): Promise<AIResponse>;
|
||||
abstract generateCompletion(prompt: string, options?: AIOptions): Promise<AIResponse>;
|
||||
abstract generateStreamingCompletion(
|
||||
prompt: string,
|
||||
options?: AIOptions
|
||||
@@ -309,9 +306,7 @@ export abstract class BaseAIProvider implements IAIProvider {
|
||||
const modelExists = availableModels.some((m) => m.id === model);
|
||||
|
||||
if (!modelExists) {
|
||||
throw new Error(
|
||||
`Model "${model}" is not available for provider "${this.getName()}"`
|
||||
);
|
||||
throw new Error(`Model "${model}" is not available for provider "${this.getName()}"`);
|
||||
}
|
||||
|
||||
this.currentModel = model;
|
||||
@@ -347,11 +342,7 @@ export abstract class BaseAIProvider implements IAIProvider {
|
||||
* @param duration - Request duration in milliseconds
|
||||
* @param success - Whether the request was successful
|
||||
*/
|
||||
protected updateUsageStats(
|
||||
response: AIResponse,
|
||||
duration: number,
|
||||
success: boolean
|
||||
): void {
|
||||
protected updateUsageStats(response: AIResponse, duration: number, success: boolean): void {
|
||||
if (!this.usageStats) return;
|
||||
|
||||
this.usageStats.totalRequests++;
|
||||
@@ -370,18 +361,15 @@ export abstract class BaseAIProvider implements IAIProvider {
|
||||
}
|
||||
|
||||
// Update average response time
|
||||
const totalTime =
|
||||
this.usageStats.averageResponseTime * (this.usageStats.totalRequests - 1);
|
||||
this.usageStats.averageResponseTime =
|
||||
(totalTime + duration) / this.usageStats.totalRequests;
|
||||
const totalTime = this.usageStats.averageResponseTime * (this.usageStats.totalRequests - 1);
|
||||
this.usageStats.averageResponseTime = (totalTime + duration) / this.usageStats.totalRequests;
|
||||
|
||||
// Update success rate
|
||||
const successCount = Math.floor(
|
||||
this.usageStats.successRate * (this.usageStats.totalRequests - 1)
|
||||
);
|
||||
const newSuccessCount = successCount + (success ? 1 : 0);
|
||||
this.usageStats.successRate =
|
||||
newSuccessCount / this.usageStats.totalRequests;
|
||||
this.usageStats.successRate = newSuccessCount / this.usageStats.totalRequests;
|
||||
|
||||
this.usageStats.lastRequestAt = new Date().toISOString();
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* This file defines the contract for configuration management
|
||||
*/
|
||||
|
||||
import type { TaskPriority, TaskComplexity } from '../types/index';
|
||||
import type { TaskComplexity, TaskPriority } from '../types/index';
|
||||
|
||||
/**
|
||||
* Model configuration for different AI roles
|
||||
|
||||
@@ -40,11 +40,7 @@ export interface IStorage {
|
||||
* @param tag - Optional tag context for the task
|
||||
* @returns Promise that resolves when update is complete
|
||||
*/
|
||||
updateTask(
|
||||
taskId: string,
|
||||
updates: Partial<Task>,
|
||||
tag?: string
|
||||
): Promise<void>;
|
||||
updateTask(taskId: string, updates: Partial<Task>, tag?: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Delete a task by ID
|
||||
@@ -177,11 +173,7 @@ export abstract class BaseStorage implements IStorage {
|
||||
abstract loadTasks(tag?: string): Promise<Task[]>;
|
||||
abstract saveTasks(tasks: Task[], tag?: string): Promise<void>;
|
||||
abstract appendTasks(tasks: Task[], tag?: string): Promise<void>;
|
||||
abstract updateTask(
|
||||
taskId: string,
|
||||
updates: Partial<Task>,
|
||||
tag?: string
|
||||
): Promise<void>;
|
||||
abstract updateTask(taskId: string, updates: Partial<Task>, tag?: string): Promise<void>;
|
||||
abstract deleteTask(taskId: string, tag?: string): Promise<void>;
|
||||
abstract exists(tag?: string): Promise<boolean>;
|
||||
abstract loadMetadata(tag?: string): Promise<TaskMetadata | null>;
|
||||
|
||||
@@ -22,9 +22,7 @@ export interface TaskParser {
|
||||
export class PlaceholderParser implements TaskParser {
|
||||
async parse(content: string): Promise<PlaceholderTask[]> {
|
||||
// Simple placeholder parsing logic
|
||||
const lines = content
|
||||
.split('\n')
|
||||
.filter((line) => line.trim().startsWith('-'));
|
||||
const lines = content.split('\n').filter((line) => line.trim().startsWith('-'));
|
||||
return lines.map((line, index) => ({
|
||||
id: `task-${index + 1}`,
|
||||
title: line.trim().replace(/^-\s*/, ''),
|
||||
|
||||
405
packages/tm-core/src/providers/ai/base-provider.ts
Normal file
405
packages/tm-core/src/providers/ai/base-provider.ts
Normal file
@@ -0,0 +1,405 @@
|
||||
/**
|
||||
* @fileoverview Abstract base provider with Template Method pattern for AI providers
|
||||
* Provides common functionality, error handling, and retry logic
|
||||
*/
|
||||
|
||||
import { ERROR_CODES, TaskMasterError } from '../../errors/task-master-error.js';
|
||||
import type { AIOptions, AIResponse, IAIProvider } from '../../interfaces/ai-provider.interface.js';
|
||||
|
||||
// Constants for retry logic
|
||||
const DEFAULT_MAX_RETRIES = 3;
|
||||
const BASE_RETRY_DELAY_MS = 1000;
|
||||
const MAX_RETRY_DELAY_MS = 32000;
|
||||
const BACKOFF_MULTIPLIER = 2;
|
||||
const JITTER_FACTOR = 0.1;
|
||||
|
||||
// Constants for validation
|
||||
const MIN_PROMPT_LENGTH = 1;
|
||||
const MAX_PROMPT_LENGTH = 100000;
|
||||
const MIN_TEMPERATURE = 0;
|
||||
const MAX_TEMPERATURE = 2;
|
||||
const MIN_MAX_TOKENS = 1;
|
||||
const MAX_MAX_TOKENS = 100000;
|
||||
|
||||
/**
|
||||
* Configuration for BaseProvider
|
||||
*/
|
||||
export interface BaseProviderConfig {
|
||||
apiKey: string;
|
||||
model?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal completion result structure
|
||||
*/
|
||||
export interface CompletionResult {
|
||||
content: string;
|
||||
inputTokens?: number;
|
||||
outputTokens?: number;
|
||||
finishReason?: string;
|
||||
model?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation result for input validation
|
||||
*/
|
||||
interface ValidationResult {
|
||||
valid: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepared request after preprocessing
|
||||
*/
|
||||
interface PreparedRequest {
|
||||
prompt: string;
|
||||
options: AIOptions;
|
||||
metadata: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract base provider implementing Template Method pattern
|
||||
* Provides common error handling, retry logic, and validation
|
||||
*/
|
||||
export abstract class BaseProvider implements IAIProvider {
|
||||
protected readonly apiKey: string;
|
||||
protected model: string;
|
||||
|
||||
constructor(config: BaseProviderConfig) {
|
||||
if (!config.apiKey) {
|
||||
throw new TaskMasterError('API key is required', ERROR_CODES.AUTHENTICATION_ERROR);
|
||||
}
|
||||
this.apiKey = config.apiKey;
|
||||
this.model = config.model || this.getDefaultModel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Template method for generating completions
|
||||
* Handles validation, retries, and error handling
|
||||
*/
|
||||
async generateCompletion(prompt: string, options?: AIOptions): Promise<AIResponse> {
|
||||
// Validate input
|
||||
const validation = this.validateInput(prompt, options);
|
||||
if (!validation.valid) {
|
||||
throw new TaskMasterError(validation.error || 'Invalid input', ERROR_CODES.VALIDATION_ERROR);
|
||||
}
|
||||
|
||||
// Prepare request
|
||||
const prepared = this.prepareRequest(prompt, options);
|
||||
|
||||
// Execute with retry logic
|
||||
let lastError: Error | undefined;
|
||||
const maxRetries = this.getMaxRetries();
|
||||
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
const result = await this.generateCompletionInternal(prepared.prompt, prepared.options);
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
return this.handleResponse(result, duration, prepared);
|
||||
} catch (error) {
|
||||
lastError = error as Error;
|
||||
|
||||
if (!this.shouldRetry(error, attempt)) {
|
||||
break;
|
||||
}
|
||||
|
||||
const delay = this.calculateBackoffDelay(attempt);
|
||||
await this.sleep(delay);
|
||||
}
|
||||
}
|
||||
|
||||
// All retries failed
|
||||
this.handleError(lastError || new Error('Unknown error'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate input prompt and options
|
||||
*/
|
||||
protected validateInput(prompt: string, options?: AIOptions): ValidationResult {
|
||||
// Validate prompt
|
||||
if (!prompt || typeof prompt !== 'string') {
|
||||
return { valid: false, error: 'Prompt must be a non-empty string' };
|
||||
}
|
||||
|
||||
const trimmedPrompt = prompt.trim();
|
||||
if (trimmedPrompt.length < MIN_PROMPT_LENGTH) {
|
||||
return { valid: false, error: 'Prompt cannot be empty' };
|
||||
}
|
||||
|
||||
if (trimmedPrompt.length > MAX_PROMPT_LENGTH) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Prompt exceeds maximum length of ${MAX_PROMPT_LENGTH} characters`
|
||||
};
|
||||
}
|
||||
|
||||
// Validate options if provided
|
||||
if (options) {
|
||||
const optionValidation = this.validateOptions(options);
|
||||
if (!optionValidation.valid) {
|
||||
return optionValidation;
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate completion options
|
||||
*/
|
||||
protected validateOptions(options: AIOptions): ValidationResult {
|
||||
if (options.temperature !== undefined) {
|
||||
if (options.temperature < MIN_TEMPERATURE || options.temperature > MAX_TEMPERATURE) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Temperature must be between ${MIN_TEMPERATURE} and ${MAX_TEMPERATURE}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (options.maxTokens !== undefined) {
|
||||
if (options.maxTokens < MIN_MAX_TOKENS || options.maxTokens > MAX_MAX_TOKENS) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Max tokens must be between ${MIN_MAX_TOKENS} and ${MAX_MAX_TOKENS}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (options.topP !== undefined) {
|
||||
if (options.topP < 0 || options.topP > 1) {
|
||||
return { valid: false, error: 'Top-p must be between 0 and 1' };
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare request for processing
|
||||
*/
|
||||
protected prepareRequest(prompt: string, options?: AIOptions): PreparedRequest {
|
||||
const defaultOptions = this.getDefaultOptions();
|
||||
const mergedOptions = { ...defaultOptions, ...options };
|
||||
|
||||
return {
|
||||
prompt: prompt.trim(),
|
||||
options: mergedOptions,
|
||||
metadata: {
|
||||
provider: this.getName(),
|
||||
model: this.model,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Process and format the response
|
||||
*/
|
||||
protected handleResponse(
|
||||
result: CompletionResult,
|
||||
duration: number,
|
||||
request: PreparedRequest
|
||||
): AIResponse {
|
||||
const inputTokens = result.inputTokens || this.calculateTokens(request.prompt);
|
||||
const outputTokens = result.outputTokens || this.calculateTokens(result.content);
|
||||
|
||||
return {
|
||||
content: result.content,
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
totalTokens: inputTokens + outputTokens,
|
||||
model: result.model || this.model,
|
||||
provider: this.getName(),
|
||||
timestamp: request.metadata.timestamp,
|
||||
duration,
|
||||
finishReason: result.finishReason
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle errors with proper wrapping
|
||||
*/
|
||||
protected handleError(error: unknown): never {
|
||||
if (error instanceof TaskMasterError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
const errorCode = this.getErrorCode(error);
|
||||
|
||||
throw new TaskMasterError(
|
||||
`${this.getName()} provider error: ${errorMessage}`,
|
||||
errorCode,
|
||||
{
|
||||
operation: 'generateCompletion',
|
||||
resource: this.getName(),
|
||||
details:
|
||||
error instanceof Error
|
||||
? {
|
||||
name: error.name,
|
||||
stack: error.stack,
|
||||
model: this.model
|
||||
}
|
||||
: { error: String(error), model: this.model }
|
||||
},
|
||||
error instanceof Error ? error : undefined
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if request should be retried
|
||||
*/
|
||||
protected shouldRetry(error: unknown, attempt: number): boolean {
|
||||
if (attempt >= this.getMaxRetries()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.isRetryableError(error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if error is retryable
|
||||
*/
|
||||
protected isRetryableError(error: unknown): boolean {
|
||||
if (this.isRateLimitError(error)) return true;
|
||||
if (this.isTimeoutError(error)) return true;
|
||||
if (this.isNetworkError(error)) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if error is a rate limit error
|
||||
*/
|
||||
protected isRateLimitError(error: unknown): boolean {
|
||||
if (error instanceof Error) {
|
||||
const message = error.message.toLowerCase();
|
||||
return (
|
||||
message.includes('rate limit') ||
|
||||
message.includes('too many requests') ||
|
||||
message.includes('429')
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if error is a timeout error
|
||||
*/
|
||||
protected isTimeoutError(error: unknown): boolean {
|
||||
if (error instanceof Error) {
|
||||
const message = error.message.toLowerCase();
|
||||
return (
|
||||
message.includes('timeout') ||
|
||||
message.includes('timed out') ||
|
||||
message.includes('econnreset')
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if error is a network error
|
||||
*/
|
||||
protected isNetworkError(error: unknown): boolean {
|
||||
if (error instanceof Error) {
|
||||
const message = error.message.toLowerCase();
|
||||
return (
|
||||
message.includes('network') ||
|
||||
message.includes('enotfound') ||
|
||||
message.includes('econnrefused')
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate exponential backoff delay with jitter
|
||||
*/
|
||||
protected calculateBackoffDelay(attempt: number): number {
|
||||
const exponentialDelay = BASE_RETRY_DELAY_MS * BACKOFF_MULTIPLIER ** (attempt - 1);
|
||||
const clampedDelay = Math.min(exponentialDelay, MAX_RETRY_DELAY_MS);
|
||||
|
||||
// Add jitter to prevent thundering herd
|
||||
const jitter = clampedDelay * JITTER_FACTOR * (Math.random() - 0.5) * 2;
|
||||
|
||||
return Math.round(clampedDelay + jitter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get error code from error
|
||||
*/
|
||||
protected getErrorCode(error: unknown): string {
|
||||
if (this.isRateLimitError(error)) return ERROR_CODES.API_ERROR;
|
||||
if (this.isTimeoutError(error)) return ERROR_CODES.NETWORK_ERROR;
|
||||
if (this.isNetworkError(error)) return ERROR_CODES.NETWORK_ERROR;
|
||||
|
||||
if (error instanceof Error && error.message.includes('401')) {
|
||||
return ERROR_CODES.AUTHENTICATION_ERROR;
|
||||
}
|
||||
|
||||
return ERROR_CODES.PROVIDER_ERROR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sleep utility for delays
|
||||
*/
|
||||
protected sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default options for completions
|
||||
*/
|
||||
protected getDefaultOptions(): AIOptions {
|
||||
return {
|
||||
temperature: 0.7,
|
||||
maxTokens: 2000,
|
||||
topP: 1.0
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get maximum retry attempts
|
||||
*/
|
||||
protected getMaxRetries(): number {
|
||||
return DEFAULT_MAX_RETRIES;
|
||||
}
|
||||
|
||||
// Public interface methods
|
||||
getModel(): string {
|
||||
return this.model;
|
||||
}
|
||||
|
||||
setModel(model: string): void {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
// Abstract methods that must be implemented by concrete providers
|
||||
protected abstract generateCompletionInternal(
|
||||
prompt: string,
|
||||
options?: AIOptions
|
||||
): Promise<CompletionResult>;
|
||||
|
||||
abstract calculateTokens(text: string, model?: string): number;
|
||||
abstract getName(): string;
|
||||
abstract getDefaultModel(): string;
|
||||
|
||||
// IAIProvider methods that must be implemented
|
||||
abstract generateStreamingCompletion(
|
||||
prompt: string,
|
||||
options?: AIOptions
|
||||
): AsyncIterator<Partial<AIResponse>>;
|
||||
abstract isAvailable(): Promise<boolean>;
|
||||
abstract getProviderInfo(): import('../../interfaces/ai-provider.interface.js').ProviderInfo;
|
||||
abstract getAvailableModels(): import('../../interfaces/ai-provider.interface.js').AIModel[];
|
||||
abstract validateCredentials(): Promise<boolean>;
|
||||
abstract getUsageStats(): Promise<
|
||||
import('../../interfaces/ai-provider.interface.js').ProviderUsageStats | null
|
||||
>;
|
||||
abstract initialize(): Promise<void>;
|
||||
abstract close(): Promise<void>;
|
||||
}
|
||||
14
packages/tm-core/src/providers/ai/index.ts
Normal file
14
packages/tm-core/src/providers/ai/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* @fileoverview Barrel export for AI provider modules
|
||||
*/
|
||||
|
||||
export { BaseProvider } from './base-provider.js';
|
||||
export type { BaseProviderConfig, CompletionResult } from './base-provider.js';
|
||||
|
||||
// Export provider factory when implemented
|
||||
// export { ProviderFactory } from './provider-factory.js';
|
||||
|
||||
// Export concrete providers when implemented
|
||||
// export { AnthropicProvider } from './adapters/anthropic-provider.js';
|
||||
// export { OpenAIProvider } from './adapters/openai-provider.js';
|
||||
// export { GoogleProvider } from './adapters/google-provider.js';
|
||||
@@ -3,11 +3,11 @@
|
||||
* Provides common functionality and properties for all AI provider implementations
|
||||
*/
|
||||
|
||||
import {
|
||||
IAIProvider,
|
||||
import type {
|
||||
AIModel,
|
||||
AIOptions,
|
||||
AIResponse,
|
||||
AIModel,
|
||||
IAIProvider,
|
||||
ProviderInfo,
|
||||
ProviderUsageStats
|
||||
} from '../interfaces/ai-provider.interface.js';
|
||||
@@ -32,9 +32,9 @@ export abstract class BaseProvider implements IAIProvider {
|
||||
/** Current model being used */
|
||||
protected model: string;
|
||||
/** Maximum number of retry attempts */
|
||||
protected maxRetries: number = 3;
|
||||
protected maxRetries = 3;
|
||||
/** Delay between retries in milliseconds */
|
||||
protected retryDelay: number = 1000;
|
||||
protected retryDelay = 1000;
|
||||
|
||||
/**
|
||||
* Constructor for BaseProvider
|
||||
@@ -54,10 +54,7 @@ export abstract class BaseProvider implements IAIProvider {
|
||||
}
|
||||
|
||||
// Abstract methods that concrete providers must implement
|
||||
abstract generateCompletion(
|
||||
prompt: string,
|
||||
options?: AIOptions
|
||||
): Promise<AIResponse>;
|
||||
abstract generateCompletion(prompt: string, options?: AIOptions): Promise<AIResponse>;
|
||||
abstract generateStreamingCompletion(
|
||||
prompt: string,
|
||||
options?: AIOptions
|
||||
|
||||
@@ -1,27 +1,19 @@
|
||||
/**
|
||||
* @fileoverview AI provider implementations for the tm-core package
|
||||
* This file exports all AI provider classes and interfaces
|
||||
* @fileoverview Barrel export for provider modules
|
||||
*/
|
||||
|
||||
// Provider interfaces and implementations
|
||||
export * from './base-provider.js';
|
||||
// export * from './anthropic-provider.js';
|
||||
// export * from './openai-provider.js';
|
||||
// export * from './perplexity-provider.js';
|
||||
// Export AI providers from subdirectory
|
||||
export { BaseProvider } from './ai/base-provider.js';
|
||||
export type {
|
||||
BaseProviderConfig,
|
||||
CompletionResult
|
||||
} from './ai/base-provider.js';
|
||||
|
||||
// Placeholder exports - these will be implemented in later tasks
|
||||
export interface AIProvider {
|
||||
name: string;
|
||||
generateResponse(prompt: string): Promise<string>;
|
||||
}
|
||||
// Export all from AI module
|
||||
export * from './ai/index.js';
|
||||
|
||||
/**
|
||||
* @deprecated This is a placeholder class that will be properly implemented in later tasks
|
||||
*/
|
||||
export class PlaceholderProvider implements AIProvider {
|
||||
name = 'placeholder';
|
||||
// Storage providers will be exported here when implemented
|
||||
// export * from './storage/index.js';
|
||||
|
||||
async generateResponse(prompt: string): Promise<string> {
|
||||
return `Placeholder response for: ${prompt}`;
|
||||
}
|
||||
}
|
||||
// Placeholder provider for tests
|
||||
export { PlaceholderProvider } from './placeholder-provider.js';
|
||||
|
||||
15
packages/tm-core/src/providers/placeholder-provider.ts
Normal file
15
packages/tm-core/src/providers/placeholder-provider.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* @fileoverview Placeholder provider for testing purposes
|
||||
* @deprecated This is a placeholder implementation that will be replaced
|
||||
*/
|
||||
|
||||
/**
|
||||
* PlaceholderProvider for smoke tests
|
||||
*/
|
||||
export class PlaceholderProvider {
|
||||
name = 'placeholder';
|
||||
|
||||
async generateResponse(prompt: string): Promise<string> {
|
||||
return `Mock response to: ${prompt}`;
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@
|
||||
* File-based storage implementation for Task Master
|
||||
*/
|
||||
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { promises as fs } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import type { Task, TaskMetadata } from '../types/index.js';
|
||||
import { BaseStorage, type StorageStats } from './storage.interface.js';
|
||||
|
||||
@@ -228,9 +228,7 @@ export class FileStorage extends BaseStorage {
|
||||
/**
|
||||
* Read and parse JSON file with error handling
|
||||
*/
|
||||
private async readJsonFile(
|
||||
filePath: string
|
||||
): Promise<FileStorageData | null> {
|
||||
private async readJsonFile(filePath: string): Promise<FileStorageData | null> {
|
||||
try {
|
||||
const content = await fs.readFile(filePath, 'utf-8');
|
||||
return JSON.parse(content);
|
||||
@@ -248,10 +246,7 @@ export class FileStorage extends BaseStorage {
|
||||
/**
|
||||
* Write JSON file with atomic operation using temp file
|
||||
*/
|
||||
private async writeJsonFile(
|
||||
filePath: string,
|
||||
data: FileStorageData
|
||||
): Promise<void> {
|
||||
private async writeJsonFile(filePath: string, data: FileStorageData): Promise<void> {
|
||||
// Use file locking to prevent concurrent writes
|
||||
const lockKey = filePath;
|
||||
const existingLock = this.fileLocks.get(lockKey);
|
||||
@@ -273,10 +268,7 @@ export class FileStorage extends BaseStorage {
|
||||
/**
|
||||
* Perform the actual write operation
|
||||
*/
|
||||
private async performWrite(
|
||||
filePath: string,
|
||||
data: FileStorageData
|
||||
): Promise<void> {
|
||||
private async performWrite(filePath: string, data: FileStorageData): Promise<void> {
|
||||
const tempPath = `${filePath}.tmp`;
|
||||
|
||||
try {
|
||||
@@ -331,9 +323,7 @@ export class FileStorage extends BaseStorage {
|
||||
try {
|
||||
const files = await fs.readdir(dir);
|
||||
const backupFiles = files
|
||||
.filter(
|
||||
(f) => f.startsWith(`${basename}.backup.`) && f.endsWith('.json')
|
||||
)
|
||||
.filter((f) => f.startsWith(`${basename}.backup.`) && f.endsWith('.json'))
|
||||
.sort()
|
||||
.reverse();
|
||||
|
||||
|
||||
@@ -2,12 +2,7 @@
|
||||
* Storage interface and base implementation for Task Master
|
||||
*/
|
||||
|
||||
import type {
|
||||
Task,
|
||||
TaskMetadata,
|
||||
TaskFilter,
|
||||
TaskSortOptions
|
||||
} from '../types/index.js';
|
||||
import type { Task, TaskFilter, TaskMetadata, TaskSortOptions } from '../types/index.js';
|
||||
|
||||
/**
|
||||
* Storage statistics
|
||||
@@ -38,11 +33,7 @@ export interface IStorage {
|
||||
loadTasks(tag?: string): Promise<Task[]>;
|
||||
saveTasks(tasks: Task[], tag?: string): Promise<void>;
|
||||
appendTasks(tasks: Task[], tag?: string): Promise<void>;
|
||||
updateTask(
|
||||
taskId: string,
|
||||
updates: Partial<Task>,
|
||||
tag?: string
|
||||
): Promise<boolean>;
|
||||
updateTask(taskId: string, updates: Partial<Task>, tag?: string): Promise<boolean>;
|
||||
deleteTask(taskId: string, tag?: string): Promise<boolean>;
|
||||
exists(tag?: string): Promise<boolean>;
|
||||
|
||||
@@ -100,11 +91,7 @@ export abstract class BaseStorage implements IStorage {
|
||||
await this.saveTasks(mergedTasks, tag);
|
||||
}
|
||||
|
||||
async updateTask(
|
||||
taskId: string,
|
||||
updates: Partial<Task>,
|
||||
tag?: string
|
||||
): Promise<boolean> {
|
||||
async updateTask(taskId: string, updates: Partial<Task>, tag?: string): Promise<boolean> {
|
||||
const tasks = await this.loadTasks(tag);
|
||||
const taskIndex = tasks.findIndex((t) => t.id === taskId);
|
||||
|
||||
@@ -149,7 +136,7 @@ export abstract class BaseStorage implements IStorage {
|
||||
};
|
||||
}
|
||||
|
||||
async saveMetadata(metadata: TaskMetadata, tag?: string): Promise<void> {
|
||||
async saveMetadata(_metadata: TaskMetadata, _tag?: string): Promise<void> {
|
||||
// Default implementation: metadata is derived from tasks
|
||||
// Subclasses can override if they store metadata separately
|
||||
}
|
||||
@@ -189,26 +176,19 @@ export abstract class BaseStorage implements IStorage {
|
||||
return tasks.filter((task) => {
|
||||
// Status filter
|
||||
if (filter.status) {
|
||||
const statuses = Array.isArray(filter.status)
|
||||
? filter.status
|
||||
: [filter.status];
|
||||
const statuses = Array.isArray(filter.status) ? filter.status : [filter.status];
|
||||
if (!statuses.includes(task.status)) return false;
|
||||
}
|
||||
|
||||
// Priority filter
|
||||
if (filter.priority) {
|
||||
const priorities = Array.isArray(filter.priority)
|
||||
? filter.priority
|
||||
: [filter.priority];
|
||||
const priorities = Array.isArray(filter.priority) ? filter.priority : [filter.priority];
|
||||
if (!priorities.includes(task.priority)) return false;
|
||||
}
|
||||
|
||||
// Tags filter
|
||||
if (filter.tags && filter.tags.length > 0) {
|
||||
if (
|
||||
!task.tags ||
|
||||
!filter.tags.some((tag) => task.tags?.includes(tag))
|
||||
) {
|
||||
if (!task.tags || !filter.tags.some((tag) => task.tags?.includes(tag))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -223,9 +203,7 @@ export abstract class BaseStorage implements IStorage {
|
||||
if (filter.search) {
|
||||
const searchLower = filter.search.toLowerCase();
|
||||
const inTitle = task.title.toLowerCase().includes(searchLower);
|
||||
const inDescription = task.description
|
||||
.toLowerCase()
|
||||
.includes(searchLower);
|
||||
const inDescription = task.description.toLowerCase().includes(searchLower);
|
||||
const inDetails = task.details.toLowerCase().includes(searchLower);
|
||||
if (!inTitle && !inDescription && !inDetails) return false;
|
||||
}
|
||||
@@ -240,8 +218,7 @@ export abstract class BaseStorage implements IStorage {
|
||||
const complexities = Array.isArray(filter.complexity)
|
||||
? filter.complexity
|
||||
: [filter.complexity];
|
||||
if (!task.complexity || !complexities.includes(task.complexity))
|
||||
return false;
|
||||
if (!task.complexity || !complexities.includes(task.complexity)) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
302
packages/tm-core/src/task-master-core.ts
Normal file
302
packages/tm-core/src/task-master-core.ts
Normal file
@@ -0,0 +1,302 @@
|
||||
/**
|
||||
* @fileoverview TaskMasterCore facade - main entry point for tm-core functionality
|
||||
*/
|
||||
|
||||
import { TaskEntity } from './core/entities/task.entity.js';
|
||||
import { ERROR_CODES, TaskMasterError } from './errors/task-master-error.js';
|
||||
import type { IConfiguration } from './interfaces/configuration.interface.js';
|
||||
import type { IStorage } from './interfaces/storage.interface.js';
|
||||
import { FileStorage } from './storage/file-storage.js';
|
||||
import type { Task, TaskFilter, TaskStatus } from './types/index.js';
|
||||
|
||||
/**
|
||||
* Options for creating TaskMasterCore instance
|
||||
*/
|
||||
export interface TaskMasterCoreOptions {
|
||||
projectPath: string;
|
||||
configuration?: Partial<IConfiguration>;
|
||||
storage?: IStorage;
|
||||
}
|
||||
|
||||
/**
|
||||
* List tasks result with metadata
|
||||
*/
|
||||
export interface ListTasksResult {
|
||||
tasks: Task[];
|
||||
total: number;
|
||||
filtered: number;
|
||||
tag?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* TaskMasterCore facade class
|
||||
* Provides simplified API for all tm-core operations
|
||||
*/
|
||||
export class TaskMasterCore {
|
||||
private storage: IStorage;
|
||||
private projectPath: string;
|
||||
private configuration: Partial<IConfiguration>;
|
||||
private initialized = false;
|
||||
|
||||
constructor(options: TaskMasterCoreOptions) {
|
||||
if (!options.projectPath) {
|
||||
throw new TaskMasterError('Project path is required', ERROR_CODES.MISSING_CONFIGURATION);
|
||||
}
|
||||
|
||||
this.projectPath = options.projectPath;
|
||||
this.configuration = options.configuration || {};
|
||||
|
||||
// Use provided storage or create default FileStorage
|
||||
this.storage = options.storage || new FileStorage(this.projectPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the TaskMasterCore instance
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
if (this.initialized) return;
|
||||
|
||||
try {
|
||||
await this.storage.initialize();
|
||||
this.initialized = true;
|
||||
} catch (error) {
|
||||
throw new TaskMasterError(
|
||||
'Failed to initialize TaskMasterCore',
|
||||
ERROR_CODES.INTERNAL_ERROR,
|
||||
{ operation: 'initialize' },
|
||||
error as Error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the instance is initialized
|
||||
*/
|
||||
private async ensureInitialized(): Promise<void> {
|
||||
if (!this.initialized) {
|
||||
await this.initialize();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List all tasks with optional filtering
|
||||
*/
|
||||
async listTasks(options?: {
|
||||
tag?: string;
|
||||
filter?: TaskFilter;
|
||||
includeSubtasks?: boolean;
|
||||
}): Promise<ListTasksResult> {
|
||||
await this.ensureInitialized();
|
||||
|
||||
try {
|
||||
// Load tasks from storage
|
||||
const rawTasks = await this.storage.loadTasks(options?.tag);
|
||||
|
||||
// Convert to TaskEntity for business logic
|
||||
const taskEntities = TaskEntity.fromArray(rawTasks);
|
||||
|
||||
// Apply filters if provided
|
||||
let filteredTasks = taskEntities;
|
||||
|
||||
if (options?.filter) {
|
||||
filteredTasks = this.applyFilters(taskEntities, options.filter);
|
||||
}
|
||||
|
||||
// Convert back to plain objects
|
||||
const tasks = filteredTasks.map((entity) => entity.toJSON());
|
||||
|
||||
// Optionally exclude subtasks
|
||||
const finalTasks =
|
||||
options?.includeSubtasks === false
|
||||
? tasks.map((task) => ({ ...task, subtasks: [] }))
|
||||
: tasks;
|
||||
|
||||
return {
|
||||
tasks: finalTasks,
|
||||
total: rawTasks.length,
|
||||
filtered: filteredTasks.length,
|
||||
tag: options?.tag
|
||||
};
|
||||
} catch (error) {
|
||||
throw new TaskMasterError(
|
||||
'Failed to list tasks',
|
||||
ERROR_CODES.INTERNAL_ERROR,
|
||||
{
|
||||
operation: 'listTasks',
|
||||
tag: options?.tag
|
||||
},
|
||||
error as Error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific task by ID
|
||||
*/
|
||||
async getTask(taskId: string, tag?: string): Promise<Task | null> {
|
||||
await this.ensureInitialized();
|
||||
|
||||
const result = await this.listTasks({ tag });
|
||||
const task = result.tasks.find((t) => t.id === taskId);
|
||||
|
||||
return task || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tasks by status
|
||||
*/
|
||||
async getTasksByStatus(status: TaskStatus | TaskStatus[], tag?: string): Promise<Task[]> {
|
||||
const statuses = Array.isArray(status) ? status : [status];
|
||||
const result = await this.listTasks({
|
||||
tag,
|
||||
filter: { status: statuses }
|
||||
});
|
||||
|
||||
return result.tasks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get task statistics
|
||||
*/
|
||||
async getTaskStats(tag?: string): Promise<{
|
||||
total: number;
|
||||
byStatus: Record<TaskStatus, number>;
|
||||
withSubtasks: number;
|
||||
blocked: number;
|
||||
}> {
|
||||
const result = await this.listTasks({ tag });
|
||||
|
||||
const stats = {
|
||||
total: result.total,
|
||||
byStatus: {} as Record<TaskStatus, number>,
|
||||
withSubtasks: 0,
|
||||
blocked: 0
|
||||
};
|
||||
|
||||
// Initialize status counts
|
||||
const statuses: TaskStatus[] = [
|
||||
'pending',
|
||||
'in-progress',
|
||||
'done',
|
||||
'deferred',
|
||||
'cancelled',
|
||||
'blocked',
|
||||
'review'
|
||||
];
|
||||
|
||||
statuses.forEach((status) => {
|
||||
stats.byStatus[status] = 0;
|
||||
});
|
||||
|
||||
// Count tasks
|
||||
result.tasks.forEach((task) => {
|
||||
stats.byStatus[task.status]++;
|
||||
|
||||
if (task.subtasks && task.subtasks.length > 0) {
|
||||
stats.withSubtasks++;
|
||||
}
|
||||
|
||||
if (task.status === 'blocked') {
|
||||
stats.blocked++;
|
||||
}
|
||||
});
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply filters to tasks
|
||||
*/
|
||||
private applyFilters(tasks: TaskEntity[], filter: TaskFilter): TaskEntity[] {
|
||||
return tasks.filter((task) => {
|
||||
// Filter by status
|
||||
if (filter.status) {
|
||||
const statuses = Array.isArray(filter.status) ? filter.status : [filter.status];
|
||||
if (!statuses.includes(task.status)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by priority
|
||||
if (filter.priority) {
|
||||
const priorities = Array.isArray(filter.priority) ? filter.priority : [filter.priority];
|
||||
if (!priorities.includes(task.priority)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by tags
|
||||
if (filter.tags && filter.tags.length > 0) {
|
||||
if (!task.tags || !filter.tags.some((tag) => task.tags?.includes(tag))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by assignee
|
||||
if (filter.assignee) {
|
||||
if (task.assignee !== filter.assignee) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by complexity
|
||||
if (filter.complexity) {
|
||||
const complexities = Array.isArray(filter.complexity)
|
||||
? filter.complexity
|
||||
: [filter.complexity];
|
||||
if (!task.complexity || !complexities.includes(task.complexity)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by search term
|
||||
if (filter.search) {
|
||||
const searchLower = filter.search.toLowerCase();
|
||||
const inTitle = task.title.toLowerCase().includes(searchLower);
|
||||
const inDescription = task.description.toLowerCase().includes(searchLower);
|
||||
const inDetails = task.details.toLowerCase().includes(searchLower);
|
||||
|
||||
if (!inTitle && !inDescription && !inDetails) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by hasSubtasks
|
||||
if (filter.hasSubtasks !== undefined) {
|
||||
const hasSubtasks = task.subtasks.length > 0;
|
||||
if (hasSubtasks !== filter.hasSubtasks) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Close and cleanup resources
|
||||
*/
|
||||
async close(): Promise<void> {
|
||||
if (this.storage) {
|
||||
await this.storage.close();
|
||||
}
|
||||
this.initialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory function to create TaskMasterCore instance
|
||||
*/
|
||||
export function createTaskMasterCore(
|
||||
projectPath: string,
|
||||
options?: {
|
||||
configuration?: Partial<IConfiguration>;
|
||||
storage?: IStorage;
|
||||
}
|
||||
): TaskMasterCore {
|
||||
return new TaskMasterCore({
|
||||
projectPath,
|
||||
configuration: options?.configuration,
|
||||
storage: options?.storage
|
||||
});
|
||||
}
|
||||
@@ -93,10 +93,7 @@ export interface TaskCollection {
|
||||
/**
|
||||
* Type for creating a new task (without generated fields)
|
||||
*/
|
||||
export type CreateTask = Omit<
|
||||
Task,
|
||||
'id' | 'createdAt' | 'updatedAt' | 'subtasks'
|
||||
> & {
|
||||
export type CreateTask = Omit<Task, 'id' | 'createdAt' | 'updatedAt' | 'subtasks'> & {
|
||||
subtasks?: Omit<Subtask, 'id' | 'parentId' | 'createdAt' | 'updatedAt'>[];
|
||||
};
|
||||
|
||||
@@ -138,15 +135,7 @@ export interface TaskSortOptions {
|
||||
export function isTaskStatus(value: unknown): value is TaskStatus {
|
||||
return (
|
||||
typeof value === 'string' &&
|
||||
[
|
||||
'pending',
|
||||
'in-progress',
|
||||
'done',
|
||||
'deferred',
|
||||
'cancelled',
|
||||
'blocked',
|
||||
'review'
|
||||
].includes(value)
|
||||
['pending', 'in-progress', 'done', 'deferred', 'cancelled', 'blocked', 'review'].includes(value)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -154,10 +143,7 @@ export function isTaskStatus(value: unknown): value is TaskStatus {
|
||||
* Type guard to check if a value is a valid TaskPriority
|
||||
*/
|
||||
export function isTaskPriority(value: unknown): value is TaskPriority {
|
||||
return (
|
||||
typeof value === 'string' &&
|
||||
['low', 'medium', 'high', 'critical'].includes(value)
|
||||
);
|
||||
return typeof value === 'string' && ['low', 'medium', 'high', 'critical'].includes(value);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -165,8 +151,7 @@ export function isTaskPriority(value: unknown): value is TaskPriority {
|
||||
*/
|
||||
export function isTaskComplexity(value: unknown): value is TaskComplexity {
|
||||
return (
|
||||
typeof value === 'string' &&
|
||||
['simple', 'moderate', 'complex', 'very-complex'].includes(value)
|
||||
typeof value === 'string' && ['simple', 'moderate', 'complex', 'very-complex'].includes(value)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Provides functions to generate unique identifiers for tasks and subtasks
|
||||
*/
|
||||
|
||||
import { randomBytes } from 'crypto';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
|
||||
/**
|
||||
* Generates a unique task ID using the format: TASK-{timestamp}-{random}
|
||||
@@ -33,28 +33,22 @@ export function generateTaskId(): string {
|
||||
* // Returns: "TASK-123-A7B3.2"
|
||||
* ```
|
||||
*/
|
||||
export function generateSubtaskId(
|
||||
parentId: string,
|
||||
existingSubtasks: string[] = []
|
||||
): string {
|
||||
export function generateSubtaskId(parentId: string, existingSubtasks: string[] = []): string {
|
||||
// Find existing subtasks for this parent
|
||||
const parentSubtasks = existingSubtasks.filter((id) =>
|
||||
id.startsWith(`${parentId}.`)
|
||||
);
|
||||
const parentSubtasks = existingSubtasks.filter((id) => id.startsWith(`${parentId}.`));
|
||||
|
||||
// Extract sequential numbers and find the highest
|
||||
const sequentialNumbers = parentSubtasks
|
||||
.map((id) => {
|
||||
const parts = id.split('.');
|
||||
const lastPart = parts[parts.length - 1];
|
||||
return parseInt(lastPart, 10);
|
||||
return Number.parseInt(lastPart, 10);
|
||||
})
|
||||
.filter((num) => !isNaN(num))
|
||||
.filter((num) => !Number.isNaN(num))
|
||||
.sort((a, b) => a - b);
|
||||
|
||||
// Determine the next sequential number
|
||||
const nextSequential =
|
||||
sequentialNumbers.length > 0 ? Math.max(...sequentialNumbers) + 1 : 1;
|
||||
const nextSequential = sequentialNumbers.length > 0 ? Math.max(...sequentialNumbers) + 1 : 1;
|
||||
|
||||
return `${parentId}.${nextSequential}`;
|
||||
}
|
||||
@@ -118,8 +112,8 @@ export function isValidSubtaskId(id: string): boolean {
|
||||
// Remaining parts should be positive integers
|
||||
const sequentialParts = parts.slice(1);
|
||||
return sequentialParts.every((part) => {
|
||||
const num = parseInt(part, 10);
|
||||
return !isNaN(num) && num > 0 && part === num.toString();
|
||||
const num = Number.parseInt(part, 10);
|
||||
return !Number.isNaN(num) && num > 0 && part === num.toString();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user