feat: initial tm-core pre-cleanup
This commit is contained in:
@@ -8,7 +8,13 @@
|
|||||||
},
|
},
|
||||||
"files": {
|
"files": {
|
||||||
"include": ["src/**/*.ts", "tests/**/*.ts", "*.ts", "*.js", "*.json"],
|
"include": ["src/**/*.ts", "tests/**/*.ts", "*.ts", "*.js", "*.json"],
|
||||||
"ignore": ["**/node_modules", "**/dist", "**/.git", "**/coverage", "**/*.d.ts"]
|
"ignore": [
|
||||||
|
"**/node_modules",
|
||||||
|
"**/dist",
|
||||||
|
"**/.git",
|
||||||
|
"**/coverage",
|
||||||
|
"**/*.d.ts"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"formatter": {
|
"formatter": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
|
|||||||
75
packages/tm-core/src/constants/index.ts
Normal file
75
packages/tm-core/src/constants/index.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Constants for Task Master Core
|
||||||
|
* Single source of truth for all constant values
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type {
|
||||||
|
TaskStatus,
|
||||||
|
TaskPriority,
|
||||||
|
TaskComplexity
|
||||||
|
} from '../types/index.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valid task status values
|
||||||
|
*/
|
||||||
|
export const TASK_STATUSES: readonly TaskStatus[] = [
|
||||||
|
'pending',
|
||||||
|
'in-progress',
|
||||||
|
'done',
|
||||||
|
'deferred',
|
||||||
|
'cancelled',
|
||||||
|
'blocked',
|
||||||
|
'review'
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valid task priority values
|
||||||
|
*/
|
||||||
|
export const TASK_PRIORITIES: readonly TaskPriority[] = [
|
||||||
|
'low',
|
||||||
|
'medium',
|
||||||
|
'high',
|
||||||
|
'critical'
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valid task complexity values
|
||||||
|
*/
|
||||||
|
export const TASK_COMPLEXITIES: readonly TaskComplexity[] = [
|
||||||
|
'simple',
|
||||||
|
'moderate',
|
||||||
|
'complex',
|
||||||
|
'very-complex'
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valid output formats for task display
|
||||||
|
*/
|
||||||
|
export const OUTPUT_FORMATS = ['text', 'json', 'compact'] as const;
|
||||||
|
export type OutputFormat = (typeof OUTPUT_FORMATS)[number];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status icons for display
|
||||||
|
*/
|
||||||
|
export const STATUS_ICONS: Record<TaskStatus, string> = {
|
||||||
|
done: '✓',
|
||||||
|
'in-progress': '►',
|
||||||
|
blocked: '⭕',
|
||||||
|
pending: '○',
|
||||||
|
deferred: '⏸',
|
||||||
|
cancelled: '✗',
|
||||||
|
review: '👁'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status colors for display (using chalk color names)
|
||||||
|
*/
|
||||||
|
export const STATUS_COLORS: Record<TaskStatus, string> = {
|
||||||
|
pending: 'yellow',
|
||||||
|
'in-progress': 'blue',
|
||||||
|
done: 'green',
|
||||||
|
deferred: 'gray',
|
||||||
|
cancelled: 'red',
|
||||||
|
blocked: 'magenta',
|
||||||
|
review: 'cyan'
|
||||||
|
} as const;
|
||||||
@@ -3,7 +3,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { ERROR_CODES, TaskMasterError } from '../errors/task-master-error.js';
|
import { ERROR_CODES, TaskMasterError } from '../errors/task-master-error.js';
|
||||||
import type { Subtask, Task, TaskPriority, TaskStatus } from '../types/index.js';
|
import type {
|
||||||
|
Subtask,
|
||||||
|
Task,
|
||||||
|
TaskPriority,
|
||||||
|
TaskStatus
|
||||||
|
} from '../types/index.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Task entity representing a task with business logic
|
* Task entity representing a task with business logic
|
||||||
@@ -64,11 +69,17 @@ export class TaskEntity implements Task {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!data.title || data.title.trim().length === 0) {
|
if (!data.title || data.title.trim().length === 0) {
|
||||||
throw new TaskMasterError('Task title is required', ERROR_CODES.VALIDATION_ERROR);
|
throw new TaskMasterError(
|
||||||
|
'Task title is required',
|
||||||
|
ERROR_CODES.VALIDATION_ERROR
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data.description || data.description.trim().length === 0) {
|
if (!data.description || data.description.trim().length === 0) {
|
||||||
throw new TaskMasterError('Task description is required', ERROR_CODES.VALIDATION_ERROR);
|
throw new TaskMasterError(
|
||||||
|
'Task description is required',
|
||||||
|
ERROR_CODES.VALIDATION_ERROR
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.isValidStatus(data.status)) {
|
if (!this.isValidStatus(data.status)) {
|
||||||
@@ -184,7 +195,10 @@ export class TaskEntity implements Task {
|
|||||||
*/
|
*/
|
||||||
updateStatus(newStatus: TaskStatus): void {
|
updateStatus(newStatus: TaskStatus): void {
|
||||||
if (!this.isValidStatus(newStatus)) {
|
if (!this.isValidStatus(newStatus)) {
|
||||||
throw new TaskMasterError(`Invalid status: ${newStatus}`, ERROR_CODES.VALIDATION_ERROR);
|
throw new TaskMasterError(
|
||||||
|
`Invalid status: ${newStatus}`,
|
||||||
|
ERROR_CODES.VALIDATION_ERROR
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Business rule: Cannot move from done to pending
|
// Business rule: Cannot move from done to pending
|
||||||
|
|||||||
@@ -215,7 +215,14 @@ export class TaskMasterError extends Error {
|
|||||||
private containsSensitiveInfo(obj: any): boolean {
|
private containsSensitiveInfo(obj: any): boolean {
|
||||||
if (typeof obj !== 'object' || obj === null) return false;
|
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();
|
const objString = JSON.stringify(obj).toLowerCase();
|
||||||
|
|
||||||
return sensitiveKeys.some((key) => objString.includes(key));
|
return sensitiveKeys.some((key) => objString.includes(key));
|
||||||
@@ -297,7 +304,9 @@ export class TaskMasterError extends Error {
|
|||||||
/**
|
/**
|
||||||
* Create a new error with additional context
|
* Create a new error with additional context
|
||||||
*/
|
*/
|
||||||
public withContext(additionalContext: Partial<ErrorContext>): TaskMasterError {
|
public withContext(
|
||||||
|
additionalContext: Partial<ErrorContext>
|
||||||
|
): TaskMasterError {
|
||||||
return new TaskMasterError(
|
return new TaskMasterError(
|
||||||
this.message,
|
this.message,
|
||||||
this.code,
|
this.code,
|
||||||
|
|||||||
@@ -17,11 +17,19 @@ export type * from './types/index';
|
|||||||
// Re-export interfaces (types only to avoid conflicts)
|
// Re-export interfaces (types only to avoid conflicts)
|
||||||
export type * from './interfaces/index';
|
export type * from './interfaces/index';
|
||||||
|
|
||||||
|
// Re-export constants
|
||||||
|
export * from './constants/index';
|
||||||
|
|
||||||
// Re-export providers
|
// Re-export providers
|
||||||
export * from './providers/index';
|
export * from './providers/index';
|
||||||
|
|
||||||
// Re-export storage (selectively to avoid conflicts)
|
// Re-export storage (selectively to avoid conflicts)
|
||||||
export { FileStorage, ApiStorage, StorageFactory, type ApiStorageConfig } from './storage/index';
|
export {
|
||||||
|
FileStorage,
|
||||||
|
ApiStorage,
|
||||||
|
StorageFactory,
|
||||||
|
type ApiStorageConfig
|
||||||
|
} from './storage/index';
|
||||||
export { PlaceholderStorage, type StorageAdapter } from './storage/index';
|
export { PlaceholderStorage, type StorageAdapter } from './storage/index';
|
||||||
|
|
||||||
// Re-export parser
|
// Re-export parser
|
||||||
|
|||||||
@@ -282,7 +282,10 @@ export abstract class BaseAIProvider implements IAIProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Abstract methods that must be implemented by concrete classes
|
// 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(
|
abstract generateStreamingCompletion(
|
||||||
prompt: string,
|
prompt: string,
|
||||||
options?: AIOptions
|
options?: AIOptions
|
||||||
@@ -306,7 +309,9 @@ export abstract class BaseAIProvider implements IAIProvider {
|
|||||||
const modelExists = availableModels.some((m) => m.id === model);
|
const modelExists = availableModels.some((m) => m.id === model);
|
||||||
|
|
||||||
if (!modelExists) {
|
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;
|
this.currentModel = model;
|
||||||
@@ -342,7 +347,11 @@ export abstract class BaseAIProvider implements IAIProvider {
|
|||||||
* @param duration - Request duration in milliseconds
|
* @param duration - Request duration in milliseconds
|
||||||
* @param success - Whether the request was successful
|
* @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;
|
if (!this.usageStats) return;
|
||||||
|
|
||||||
this.usageStats.totalRequests++;
|
this.usageStats.totalRequests++;
|
||||||
@@ -361,15 +370,18 @@ export abstract class BaseAIProvider implements IAIProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update average response time
|
// Update average response time
|
||||||
const totalTime = this.usageStats.averageResponseTime * (this.usageStats.totalRequests - 1);
|
const totalTime =
|
||||||
this.usageStats.averageResponseTime = (totalTime + duration) / this.usageStats.totalRequests;
|
this.usageStats.averageResponseTime * (this.usageStats.totalRequests - 1);
|
||||||
|
this.usageStats.averageResponseTime =
|
||||||
|
(totalTime + duration) / this.usageStats.totalRequests;
|
||||||
|
|
||||||
// Update success rate
|
// Update success rate
|
||||||
const successCount = Math.floor(
|
const successCount = Math.floor(
|
||||||
this.usageStats.successRate * (this.usageStats.totalRequests - 1)
|
this.usageStats.successRate * (this.usageStats.totalRequests - 1)
|
||||||
);
|
);
|
||||||
const newSuccessCount = successCount + (success ? 1 : 0);
|
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();
|
this.usageStats.lastRequestAt = new Date().toISOString();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,11 @@ export interface IStorage {
|
|||||||
* @param tag - Optional tag context for the task
|
* @param tag - Optional tag context for the task
|
||||||
* @returns Promise that resolves when update is complete
|
* @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
|
* Delete a task by ID
|
||||||
@@ -173,7 +177,11 @@ export abstract class BaseStorage implements IStorage {
|
|||||||
abstract loadTasks(tag?: string): Promise<Task[]>;
|
abstract loadTasks(tag?: string): Promise<Task[]>;
|
||||||
abstract saveTasks(tasks: Task[], tag?: string): Promise<void>;
|
abstract saveTasks(tasks: Task[], tag?: string): Promise<void>;
|
||||||
abstract appendTasks(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 deleteTask(taskId: string, tag?: string): Promise<void>;
|
||||||
abstract exists(tag?: string): Promise<boolean>;
|
abstract exists(tag?: string): Promise<boolean>;
|
||||||
abstract loadMetadata(tag?: string): Promise<TaskMetadata | null>;
|
abstract loadMetadata(tag?: string): Promise<TaskMetadata | null>;
|
||||||
|
|||||||
@@ -22,7 +22,9 @@ export interface TaskParser {
|
|||||||
export class PlaceholderParser implements TaskParser {
|
export class PlaceholderParser implements TaskParser {
|
||||||
async parse(content: string): Promise<PlaceholderTask[]> {
|
async parse(content: string): Promise<PlaceholderTask[]> {
|
||||||
// Simple placeholder parsing logic
|
// 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) => ({
|
return lines.map((line, index) => ({
|
||||||
id: `task-${index + 1}`,
|
id: `task-${index + 1}`,
|
||||||
title: line.trim().replace(/^-\s*/, ''),
|
title: line.trim().replace(/^-\s*/, ''),
|
||||||
|
|||||||
@@ -3,8 +3,15 @@
|
|||||||
* Provides common functionality, error handling, and retry logic
|
* Provides common functionality, error handling, and retry logic
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ERROR_CODES, TaskMasterError } from '../../errors/task-master-error.js';
|
import {
|
||||||
import type { AIOptions, AIResponse, IAIProvider } from '../../interfaces/ai-provider.interface.js';
|
ERROR_CODES,
|
||||||
|
TaskMasterError
|
||||||
|
} from '../../errors/task-master-error.js';
|
||||||
|
import type {
|
||||||
|
AIOptions,
|
||||||
|
AIResponse,
|
||||||
|
IAIProvider
|
||||||
|
} from '../../interfaces/ai-provider.interface.js';
|
||||||
|
|
||||||
// Constants for retry logic
|
// Constants for retry logic
|
||||||
const DEFAULT_MAX_RETRIES = 3;
|
const DEFAULT_MAX_RETRIES = 3;
|
||||||
@@ -67,7 +74,10 @@ export abstract class BaseProvider implements IAIProvider {
|
|||||||
|
|
||||||
constructor(config: BaseProviderConfig) {
|
constructor(config: BaseProviderConfig) {
|
||||||
if (!config.apiKey) {
|
if (!config.apiKey) {
|
||||||
throw new TaskMasterError('API key is required', ERROR_CODES.AUTHENTICATION_ERROR);
|
throw new TaskMasterError(
|
||||||
|
'API key is required',
|
||||||
|
ERROR_CODES.AUTHENTICATION_ERROR
|
||||||
|
);
|
||||||
}
|
}
|
||||||
this.apiKey = config.apiKey;
|
this.apiKey = config.apiKey;
|
||||||
this.model = config.model || this.getDefaultModel();
|
this.model = config.model || this.getDefaultModel();
|
||||||
@@ -77,11 +87,17 @@ export abstract class BaseProvider implements IAIProvider {
|
|||||||
* Template method for generating completions
|
* Template method for generating completions
|
||||||
* Handles validation, retries, and error handling
|
* Handles validation, retries, and error handling
|
||||||
*/
|
*/
|
||||||
async generateCompletion(prompt: string, options?: AIOptions): Promise<AIResponse> {
|
async generateCompletion(
|
||||||
|
prompt: string,
|
||||||
|
options?: AIOptions
|
||||||
|
): Promise<AIResponse> {
|
||||||
// Validate input
|
// Validate input
|
||||||
const validation = this.validateInput(prompt, options);
|
const validation = this.validateInput(prompt, options);
|
||||||
if (!validation.valid) {
|
if (!validation.valid) {
|
||||||
throw new TaskMasterError(validation.error || 'Invalid input', ERROR_CODES.VALIDATION_ERROR);
|
throw new TaskMasterError(
|
||||||
|
validation.error || 'Invalid input',
|
||||||
|
ERROR_CODES.VALIDATION_ERROR
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare request
|
// Prepare request
|
||||||
@@ -94,7 +110,10 @@ export abstract class BaseProvider implements IAIProvider {
|
|||||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||||
try {
|
try {
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
const result = await this.generateCompletionInternal(prepared.prompt, prepared.options);
|
const result = await this.generateCompletionInternal(
|
||||||
|
prepared.prompt,
|
||||||
|
prepared.options
|
||||||
|
);
|
||||||
|
|
||||||
const duration = Date.now() - startTime;
|
const duration = Date.now() - startTime;
|
||||||
return this.handleResponse(result, duration, prepared);
|
return this.handleResponse(result, duration, prepared);
|
||||||
@@ -117,7 +136,10 @@ export abstract class BaseProvider implements IAIProvider {
|
|||||||
/**
|
/**
|
||||||
* Validate input prompt and options
|
* Validate input prompt and options
|
||||||
*/
|
*/
|
||||||
protected validateInput(prompt: string, options?: AIOptions): ValidationResult {
|
protected validateInput(
|
||||||
|
prompt: string,
|
||||||
|
options?: AIOptions
|
||||||
|
): ValidationResult {
|
||||||
// Validate prompt
|
// Validate prompt
|
||||||
if (!prompt || typeof prompt !== 'string') {
|
if (!prompt || typeof prompt !== 'string') {
|
||||||
return { valid: false, error: 'Prompt must be a non-empty string' };
|
return { valid: false, error: 'Prompt must be a non-empty string' };
|
||||||
@@ -151,7 +173,10 @@ export abstract class BaseProvider implements IAIProvider {
|
|||||||
*/
|
*/
|
||||||
protected validateOptions(options: AIOptions): ValidationResult {
|
protected validateOptions(options: AIOptions): ValidationResult {
|
||||||
if (options.temperature !== undefined) {
|
if (options.temperature !== undefined) {
|
||||||
if (options.temperature < MIN_TEMPERATURE || options.temperature > MAX_TEMPERATURE) {
|
if (
|
||||||
|
options.temperature < MIN_TEMPERATURE ||
|
||||||
|
options.temperature > MAX_TEMPERATURE
|
||||||
|
) {
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
error: `Temperature must be between ${MIN_TEMPERATURE} and ${MAX_TEMPERATURE}`
|
error: `Temperature must be between ${MIN_TEMPERATURE} and ${MAX_TEMPERATURE}`
|
||||||
@@ -160,7 +185,10 @@ export abstract class BaseProvider implements IAIProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (options.maxTokens !== undefined) {
|
if (options.maxTokens !== undefined) {
|
||||||
if (options.maxTokens < MIN_MAX_TOKENS || options.maxTokens > MAX_MAX_TOKENS) {
|
if (
|
||||||
|
options.maxTokens < MIN_MAX_TOKENS ||
|
||||||
|
options.maxTokens > MAX_MAX_TOKENS
|
||||||
|
) {
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
error: `Max tokens must be between ${MIN_MAX_TOKENS} and ${MAX_MAX_TOKENS}`
|
error: `Max tokens must be between ${MIN_MAX_TOKENS} and ${MAX_MAX_TOKENS}`
|
||||||
@@ -180,7 +208,10 @@ export abstract class BaseProvider implements IAIProvider {
|
|||||||
/**
|
/**
|
||||||
* Prepare request for processing
|
* Prepare request for processing
|
||||||
*/
|
*/
|
||||||
protected prepareRequest(prompt: string, options?: AIOptions): PreparedRequest {
|
protected prepareRequest(
|
||||||
|
prompt: string,
|
||||||
|
options?: AIOptions
|
||||||
|
): PreparedRequest {
|
||||||
const defaultOptions = this.getDefaultOptions();
|
const defaultOptions = this.getDefaultOptions();
|
||||||
const mergedOptions = { ...defaultOptions, ...options };
|
const mergedOptions = { ...defaultOptions, ...options };
|
||||||
|
|
||||||
@@ -203,8 +234,10 @@ export abstract class BaseProvider implements IAIProvider {
|
|||||||
duration: number,
|
duration: number,
|
||||||
request: PreparedRequest
|
request: PreparedRequest
|
||||||
): AIResponse {
|
): AIResponse {
|
||||||
const inputTokens = result.inputTokens || this.calculateTokens(request.prompt);
|
const inputTokens =
|
||||||
const outputTokens = result.outputTokens || this.calculateTokens(result.content);
|
result.inputTokens || this.calculateTokens(request.prompt);
|
||||||
|
const outputTokens =
|
||||||
|
result.outputTokens || this.calculateTokens(result.content);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: result.content,
|
content: result.content,
|
||||||
@@ -320,7 +353,8 @@ export abstract class BaseProvider implements IAIProvider {
|
|||||||
* Calculate exponential backoff delay with jitter
|
* Calculate exponential backoff delay with jitter
|
||||||
*/
|
*/
|
||||||
protected calculateBackoffDelay(attempt: number): number {
|
protected calculateBackoffDelay(attempt: number): number {
|
||||||
const exponentialDelay = BASE_RETRY_DELAY_MS * BACKOFF_MULTIPLIER ** (attempt - 1);
|
const exponentialDelay =
|
||||||
|
BASE_RETRY_DELAY_MS * BACKOFF_MULTIPLIER ** (attempt - 1);
|
||||||
const clampedDelay = Math.min(exponentialDelay, MAX_RETRY_DELAY_MS);
|
const clampedDelay = Math.min(exponentialDelay, MAX_RETRY_DELAY_MS);
|
||||||
|
|
||||||
// Add jitter to prevent thundering herd
|
// Add jitter to prevent thundering herd
|
||||||
@@ -394,11 +428,16 @@ export abstract class BaseProvider implements IAIProvider {
|
|||||||
options?: AIOptions
|
options?: AIOptions
|
||||||
): AsyncIterator<Partial<AIResponse>>;
|
): AsyncIterator<Partial<AIResponse>>;
|
||||||
abstract isAvailable(): Promise<boolean>;
|
abstract isAvailable(): Promise<boolean>;
|
||||||
abstract getProviderInfo(): import('../../interfaces/ai-provider.interface.js').ProviderInfo;
|
abstract getProviderInfo(): import(
|
||||||
abstract getAvailableModels(): import('../../interfaces/ai-provider.interface.js').AIModel[];
|
'../../interfaces/ai-provider.interface.js'
|
||||||
|
).ProviderInfo;
|
||||||
|
abstract getAvailableModels(): import(
|
||||||
|
'../../interfaces/ai-provider.interface.js'
|
||||||
|
).AIModel[];
|
||||||
abstract validateCredentials(): Promise<boolean>;
|
abstract validateCredentials(): Promise<boolean>;
|
||||||
abstract getUsageStats(): Promise<
|
abstract getUsageStats(): Promise<
|
||||||
import('../../interfaces/ai-provider.interface.js').ProviderUsageStats | null
|
| import('../../interfaces/ai-provider.interface.js').ProviderUsageStats
|
||||||
|
| null
|
||||||
>;
|
>;
|
||||||
abstract initialize(): Promise<void>;
|
abstract initialize(): Promise<void>;
|
||||||
abstract close(): Promise<void>;
|
abstract close(): Promise<void>;
|
||||||
|
|||||||
@@ -103,11 +103,11 @@ export class TaskService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Convert back to plain objects
|
// Convert back to plain objects
|
||||||
let tasks = filteredEntities.map(entity => entity.toJSON());
|
let tasks = filteredEntities.map((entity) => entity.toJSON());
|
||||||
|
|
||||||
// Handle subtasks option
|
// Handle subtasks option
|
||||||
if (options.includeSubtasks === false) {
|
if (options.includeSubtasks === false) {
|
||||||
tasks = tasks.map(task => ({
|
tasks = tasks.map((task) => ({
|
||||||
...task,
|
...task,
|
||||||
subtasks: []
|
subtasks: []
|
||||||
}));
|
}));
|
||||||
@@ -143,7 +143,7 @@ export class TaskService {
|
|||||||
includeSubtasks: true
|
includeSubtasks: true
|
||||||
});
|
});
|
||||||
|
|
||||||
return result.tasks.find(t => t.id === taskId) || null;
|
return result.tasks.find((t) => t.id === taskId) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -188,16 +188,21 @@ export class TaskService {
|
|||||||
|
|
||||||
// Initialize all statuses
|
// Initialize all statuses
|
||||||
const allStatuses: TaskStatus[] = [
|
const allStatuses: TaskStatus[] = [
|
||||||
'pending', 'in-progress', 'done',
|
'pending',
|
||||||
'deferred', 'cancelled', 'blocked', 'review'
|
'in-progress',
|
||||||
|
'done',
|
||||||
|
'deferred',
|
||||||
|
'cancelled',
|
||||||
|
'blocked',
|
||||||
|
'review'
|
||||||
];
|
];
|
||||||
|
|
||||||
allStatuses.forEach(status => {
|
allStatuses.forEach((status) => {
|
||||||
stats.byStatus[status] = 0;
|
stats.byStatus[status] = 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Count tasks
|
// Count tasks
|
||||||
result.tasks.forEach(task => {
|
result.tasks.forEach((task) => {
|
||||||
stats.byStatus[task.status]++;
|
stats.byStatus[task.status]++;
|
||||||
|
|
||||||
if (task.subtasks && task.subtasks.length > 0) {
|
if (task.subtasks && task.subtasks.length > 0) {
|
||||||
@@ -225,12 +230,10 @@ export class TaskService {
|
|||||||
|
|
||||||
// Find tasks with no dependencies or all dependencies satisfied
|
// Find tasks with no dependencies or all dependencies satisfied
|
||||||
const completedIds = new Set(
|
const completedIds = new Set(
|
||||||
result.tasks
|
result.tasks.filter((t) => t.status === 'done').map((t) => t.id)
|
||||||
.filter(t => t.status === 'done')
|
|
||||||
.map(t => t.id)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const availableTasks = result.tasks.filter(task => {
|
const availableTasks = result.tasks.filter((task) => {
|
||||||
if (task.status === 'done' || task.status === 'blocked') {
|
if (task.status === 'done' || task.status === 'blocked') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -239,7 +242,7 @@ export class TaskService {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return task.dependencies.every(depId =>
|
return task.dependencies.every((depId) =>
|
||||||
completedIds.has(depId.toString())
|
completedIds.has(depId.toString())
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -259,10 +262,12 @@ export class TaskService {
|
|||||||
* Apply filters to task entities
|
* Apply filters to task entities
|
||||||
*/
|
*/
|
||||||
private applyFilters(tasks: TaskEntity[], filter: TaskFilter): TaskEntity[] {
|
private applyFilters(tasks: TaskEntity[], filter: TaskFilter): TaskEntity[] {
|
||||||
return tasks.filter(task => {
|
return tasks.filter((task) => {
|
||||||
// Status filter
|
// Status filter
|
||||||
if (filter.status) {
|
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)) {
|
if (!statuses.includes(task.status)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -270,7 +275,9 @@ export class TaskService {
|
|||||||
|
|
||||||
// Priority filter
|
// Priority filter
|
||||||
if (filter.priority) {
|
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)) {
|
if (!priorities.includes(task.priority)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -278,7 +285,10 @@ export class TaskService {
|
|||||||
|
|
||||||
// Tags filter
|
// Tags filter
|
||||||
if (filter.tags && filter.tags.length > 0) {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -304,7 +314,9 @@ export class TaskService {
|
|||||||
if (filter.search) {
|
if (filter.search) {
|
||||||
const searchLower = filter.search.toLowerCase();
|
const searchLower = filter.search.toLowerCase();
|
||||||
const inTitle = task.title.toLowerCase().includes(searchLower);
|
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);
|
const inDetails = task.details.toLowerCase().includes(searchLower);
|
||||||
|
|
||||||
if (!inTitle && !inDescription && !inDetails) {
|
if (!inTitle && !inDescription && !inDetails) {
|
||||||
|
|||||||
@@ -3,7 +3,10 @@
|
|||||||
* This provides storage via REST API instead of local file system
|
* This provides storage via REST API instead of local file system
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { IStorage, StorageStats } from '../interfaces/storage.interface.js';
|
import type {
|
||||||
|
IStorage,
|
||||||
|
StorageStats
|
||||||
|
} from '../interfaces/storage.interface.js';
|
||||||
import type { Task, TaskMetadata } from '../types/index.js';
|
import type { Task, TaskMetadata } from '../types/index.js';
|
||||||
import { ERROR_CODES, TaskMasterError } from '../errors/task-master-error.js';
|
import { ERROR_CODES, TaskMasterError } from '../errors/task-master-error.js';
|
||||||
|
|
||||||
@@ -291,7 +294,9 @@ export class ApiStorage implements IStorage {
|
|||||||
? `/projects/${this.config.projectId}/metadata?tag=${encodeURIComponent(tag)}`
|
? `/projects/${this.config.projectId}/metadata?tag=${encodeURIComponent(tag)}`
|
||||||
: `/projects/${this.config.projectId}/metadata`;
|
: `/projects/${this.config.projectId}/metadata`;
|
||||||
|
|
||||||
const response = await this.makeRequest<{ metadata: TaskMetadata }>(endpoint);
|
const response = await this.makeRequest<{ metadata: TaskMetadata }>(
|
||||||
|
endpoint
|
||||||
|
);
|
||||||
|
|
||||||
if (!response.success) {
|
if (!response.success) {
|
||||||
return null;
|
return null;
|
||||||
@@ -374,7 +379,11 @@ export class ApiStorage implements IStorage {
|
|||||||
/**
|
/**
|
||||||
* Update a specific task
|
* Update a specific task
|
||||||
*/
|
*/
|
||||||
async updateTask(taskId: string, updates: Partial<Task>, tag?: string): Promise<void> {
|
async updateTask(
|
||||||
|
taskId: string,
|
||||||
|
updates: Partial<Task>,
|
||||||
|
tag?: string
|
||||||
|
): Promise<void> {
|
||||||
await this.ensureInitialized();
|
await this.ensureInitialized();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -500,13 +509,15 @@ export class ApiStorage implements IStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Return stats or default values
|
// Return stats or default values
|
||||||
return response.data?.stats || {
|
return (
|
||||||
|
response.data?.stats || {
|
||||||
totalTasks: 0,
|
totalTasks: 0,
|
||||||
totalTags: 0,
|
totalTags: 0,
|
||||||
storageSize: 0,
|
storageSize: 0,
|
||||||
lastModified: new Date().toISOString(),
|
lastModified: new Date().toISOString(),
|
||||||
tagStats: []
|
tagStats: []
|
||||||
};
|
}
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TaskMasterError(
|
throw new TaskMasterError(
|
||||||
'Failed to get stats from API',
|
'Failed to get stats from API',
|
||||||
@@ -627,9 +638,9 @@ export class ApiStorage implements IStorage {
|
|||||||
const options: RequestInit = {
|
const options: RequestInit = {
|
||||||
method,
|
method,
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${this.config.accessToken}`,
|
Authorization: `Bearer ${this.config.accessToken}`,
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Accept': 'application/json'
|
Accept: 'application/json'
|
||||||
},
|
},
|
||||||
signal: controller.signal
|
signal: controller.signal
|
||||||
};
|
};
|
||||||
@@ -678,7 +689,10 @@ export class ApiStorage implements IStorage {
|
|||||||
const errorData = data as any;
|
const errorData = data as any;
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: errorData.error || errorData.message || `HTTP ${response.status}: ${response.statusText}`
|
error:
|
||||||
|
errorData.error ||
|
||||||
|
errorData.message ||
|
||||||
|
`HTTP ${response.status}: ${response.statusText}`
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
lastError = error as Error;
|
lastError = error as Error;
|
||||||
@@ -705,6 +719,6 @@ export class ApiStorage implements IStorage {
|
|||||||
* Delay helper for retries
|
* Delay helper for retries
|
||||||
*/
|
*/
|
||||||
private delay(ms: number): Promise<void> {
|
private delay(ms: number): Promise<void> {
|
||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,7 +5,10 @@
|
|||||||
import { promises as fs } from 'node:fs';
|
import { promises as fs } from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import type { Task, TaskMetadata } from '../types/index.js';
|
import type { Task, TaskMetadata } from '../types/index.js';
|
||||||
import type { IStorage, StorageStats } from '../interfaces/storage.interface.js';
|
import type {
|
||||||
|
IStorage,
|
||||||
|
StorageStats
|
||||||
|
} from '../interfaces/storage.interface.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* File storage data structure
|
* File storage data structure
|
||||||
@@ -80,7 +83,7 @@ export class FileStorage implements IStorage {
|
|||||||
totalTags: tags.length,
|
totalTags: tags.length,
|
||||||
lastModified: lastModified || new Date().toISOString(),
|
lastModified: lastModified || new Date().toISOString(),
|
||||||
storageSize: 0, // Could calculate actual file sizes if needed
|
storageSize: 0, // Could calculate actual file sizes if needed
|
||||||
tagStats: tags.map(tag => ({
|
tagStats: tags.map((tag) => ({
|
||||||
tag,
|
tag,
|
||||||
taskCount: 0, // Would need to load each tag to get accurate count
|
taskCount: 0, // Would need to load each tag to get accurate count
|
||||||
lastModified: lastModified || new Date().toISOString()
|
lastModified: lastModified || new Date().toISOString()
|
||||||
@@ -218,9 +221,13 @@ export class FileStorage implements IStorage {
|
|||||||
/**
|
/**
|
||||||
* Update a specific task
|
* Update a specific task
|
||||||
*/
|
*/
|
||||||
async updateTask(taskId: string, updates: Partial<Task>, tag?: string): Promise<void> {
|
async updateTask(
|
||||||
|
taskId: string,
|
||||||
|
updates: Partial<Task>,
|
||||||
|
tag?: string
|
||||||
|
): Promise<void> {
|
||||||
const tasks = await this.loadTasks(tag);
|
const tasks = await this.loadTasks(tag);
|
||||||
const taskIndex = tasks.findIndex(t => t.id === taskId);
|
const taskIndex = tasks.findIndex((t) => t.id === taskId);
|
||||||
|
|
||||||
if (taskIndex === -1) {
|
if (taskIndex === -1) {
|
||||||
throw new Error(`Task ${taskId} not found`);
|
throw new Error(`Task ${taskId} not found`);
|
||||||
@@ -235,7 +242,7 @@ export class FileStorage implements IStorage {
|
|||||||
*/
|
*/
|
||||||
async deleteTask(taskId: string, tag?: string): Promise<void> {
|
async deleteTask(taskId: string, tag?: string): Promise<void> {
|
||||||
const tasks = await this.loadTasks(tag);
|
const tasks = await this.loadTasks(tag);
|
||||||
const filteredTasks = tasks.filter(t => t.id !== taskId);
|
const filteredTasks = tasks.filter((t) => t.id !== taskId);
|
||||||
|
|
||||||
if (filteredTasks.length === tasks.length) {
|
if (filteredTasks.length === tasks.length) {
|
||||||
throw new Error(`Task ${taskId} not found`);
|
throw new Error(`Task ${taskId} not found`);
|
||||||
@@ -268,7 +275,9 @@ export class FileStorage implements IStorage {
|
|||||||
try {
|
try {
|
||||||
await fs.rename(oldPath, newPath);
|
await fs.rename(oldPath, newPath);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
throw new Error(`Failed to rename tag from ${oldTag} to ${newTag}: ${error.message}`);
|
throw new Error(
|
||||||
|
`Failed to rename tag from ${oldTag} to ${newTag}: ${error.message}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,7 +332,9 @@ export class FileStorage implements IStorage {
|
|||||||
/**
|
/**
|
||||||
* Read and parse JSON file with error handling
|
* 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 {
|
try {
|
||||||
const content = await fs.readFile(filePath, 'utf-8');
|
const content = await fs.readFile(filePath, 'utf-8');
|
||||||
return JSON.parse(content);
|
return JSON.parse(content);
|
||||||
@@ -341,7 +352,10 @@ export class FileStorage implements IStorage {
|
|||||||
/**
|
/**
|
||||||
* Write JSON file with atomic operation using temp file
|
* 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
|
// Use file locking to prevent concurrent writes
|
||||||
const lockKey = filePath;
|
const lockKey = filePath;
|
||||||
const existingLock = this.fileLocks.get(lockKey);
|
const existingLock = this.fileLocks.get(lockKey);
|
||||||
@@ -363,7 +377,10 @@ export class FileStorage implements IStorage {
|
|||||||
/**
|
/**
|
||||||
* Perform the actual write operation
|
* 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`;
|
const tempPath = `${filePath}.tmp`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -432,7 +449,9 @@ export class FileStorage implements IStorage {
|
|||||||
try {
|
try {
|
||||||
const files = await fs.readdir(dir);
|
const files = await fs.readdir(dir);
|
||||||
const backupFiles = files
|
const backupFiles = files
|
||||||
.filter((f) => f.startsWith(`${basename}.backup.`) && f.endsWith('.json'))
|
.filter(
|
||||||
|
(f) => f.startsWith(`${basename}.backup.`) && f.endsWith('.json')
|
||||||
|
)
|
||||||
.sort()
|
.sort()
|
||||||
.reverse();
|
.reverse();
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ export { ApiStorage, type ApiStorageConfig } from './api-storage.js';
|
|||||||
export { StorageFactory } from './storage-factory.js';
|
export { StorageFactory } from './storage-factory.js';
|
||||||
|
|
||||||
// Export storage interface and types
|
// Export storage interface and types
|
||||||
export type { IStorage, StorageStats } from '../interfaces/storage.interface.js';
|
export type {
|
||||||
|
IStorage,
|
||||||
|
StorageStats
|
||||||
|
} from '../interfaces/storage.interface.js';
|
||||||
|
|
||||||
// Placeholder exports - these will be implemented in later tasks
|
// Placeholder exports - these will be implemented in later tasks
|
||||||
export interface StorageAdapter {
|
export interface StorageAdapter {
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
* @fileoverview Storage factory for creating appropriate storage implementations
|
* @fileoverview Storage factory for creating appropriate storage implementations
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { IStorage } from "../interfaces/storage.interface.js";
|
import type { IStorage } from '../interfaces/storage.interface.js';
|
||||||
import type { IConfiguration } from "../interfaces/configuration.interface.js";
|
import type { IConfiguration } from '../interfaces/configuration.interface.js';
|
||||||
import { FileStorage } from "./file-storage.js";
|
import { FileStorage } from './file-storage.js';
|
||||||
import { ApiStorage } from "./api-storage.js";
|
import { ApiStorage } from './api-storage.js';
|
||||||
import { ERROR_CODES, TaskMasterError } from "../errors/task-master-error.js";
|
import { ERROR_CODES, TaskMasterError } from '../errors/task-master-error.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Factory for creating storage implementations based on configuration
|
* Factory for creating storage implementations based on configuration
|
||||||
@@ -22,13 +22,13 @@ export class StorageFactory {
|
|||||||
config: Partial<IConfiguration>,
|
config: Partial<IConfiguration>,
|
||||||
projectPath: string
|
projectPath: string
|
||||||
): IStorage {
|
): IStorage {
|
||||||
const storageType = config.storage?.type || "file";
|
const storageType = config.storage?.type || 'file';
|
||||||
|
|
||||||
switch (storageType) {
|
switch (storageType) {
|
||||||
case "file":
|
case 'file':
|
||||||
return StorageFactory.createFileStorage(projectPath, config);
|
return StorageFactory.createFileStorage(projectPath, config);
|
||||||
|
|
||||||
case "api":
|
case 'api':
|
||||||
return StorageFactory.createApiStorage(config);
|
return StorageFactory.createApiStorage(config);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -59,17 +59,17 @@ export class StorageFactory {
|
|||||||
|
|
||||||
if (!apiEndpoint) {
|
if (!apiEndpoint) {
|
||||||
throw new TaskMasterError(
|
throw new TaskMasterError(
|
||||||
"API endpoint is required for API storage",
|
'API endpoint is required for API storage',
|
||||||
ERROR_CODES.MISSING_CONFIGURATION,
|
ERROR_CODES.MISSING_CONFIGURATION,
|
||||||
{ storageType: "api" }
|
{ storageType: 'api' }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!apiAccessToken) {
|
if (!apiAccessToken) {
|
||||||
throw new TaskMasterError(
|
throw new TaskMasterError(
|
||||||
"API access token is required for API storage",
|
'API access token is required for API storage',
|
||||||
ERROR_CODES.MISSING_CONFIGURATION,
|
ERROR_CODES.MISSING_CONFIGURATION,
|
||||||
{ storageType: "api" }
|
{ storageType: 'api' }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,21 +79,21 @@ export class StorageFactory {
|
|||||||
projectId: config.projectPath,
|
projectId: config.projectPath,
|
||||||
timeout: config.retry?.requestTimeout,
|
timeout: config.retry?.requestTimeout,
|
||||||
enableRetry: config.retry?.retryOnNetworkError,
|
enableRetry: config.retry?.retryOnNetworkError,
|
||||||
maxRetries: config.retry?.retryAttempts,
|
maxRetries: config.retry?.retryAttempts
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detect optimal storage type based on available configuration
|
* Detect optimal storage type based on available configuration
|
||||||
*/
|
*/
|
||||||
static detectOptimalStorage(config: Partial<IConfiguration>): "file" | "api" {
|
static detectOptimalStorage(config: Partial<IConfiguration>): 'file' | 'api' {
|
||||||
// If API credentials are provided, prefer API storage (Hamster)
|
// If API credentials are provided, prefer API storage (Hamster)
|
||||||
if (config.storage?.apiEndpoint && config.storage?.apiAccessToken) {
|
if (config.storage?.apiEndpoint && config.storage?.apiAccessToken) {
|
||||||
return "api";
|
return 'api';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default to file storage
|
// Default to file storage
|
||||||
return "file";
|
return 'file';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -107,21 +107,21 @@ export class StorageFactory {
|
|||||||
const storageType = config.storage?.type;
|
const storageType = config.storage?.type;
|
||||||
|
|
||||||
if (!storageType) {
|
if (!storageType) {
|
||||||
errors.push("Storage type is not specified");
|
errors.push('Storage type is not specified');
|
||||||
return { isValid: false, errors };
|
return { isValid: false, errors };
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (storageType) {
|
switch (storageType) {
|
||||||
case "api":
|
case 'api':
|
||||||
if (!config.storage?.apiEndpoint) {
|
if (!config.storage?.apiEndpoint) {
|
||||||
errors.push("API endpoint is required for API storage");
|
errors.push('API endpoint is required for API storage');
|
||||||
}
|
}
|
||||||
if (!config.storage?.apiAccessToken) {
|
if (!config.storage?.apiAccessToken) {
|
||||||
errors.push("API access token is required for API storage");
|
errors.push('API access token is required for API storage');
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "file":
|
case 'file':
|
||||||
// File storage doesn't require additional config
|
// File storage doesn't require additional config
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -131,7 +131,7 @@ export class StorageFactory {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
isValid: errors.length === 0,
|
isValid: errors.length === 0,
|
||||||
errors,
|
errors
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,7 +158,7 @@ export class StorageFactory {
|
|||||||
return apiStorage;
|
return apiStorage;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(
|
console.warn(
|
||||||
"Failed to initialize API storage, falling back to file storage:",
|
'Failed to initialize API storage, falling back to file storage:',
|
||||||
error
|
error
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { ConfigManager } from './config/config-manager.js';
|
import { ConfigManager } from './config/config-manager.js';
|
||||||
import { TaskService, type TaskListResult as ListTasksResult, type GetTaskListOptions } from './services/task-service.js';
|
import {
|
||||||
|
TaskService,
|
||||||
|
type TaskListResult as ListTasksResult,
|
||||||
|
type GetTaskListOptions
|
||||||
|
} from './services/task-service.js';
|
||||||
import { ERROR_CODES, TaskMasterError } from './errors/task-master-error.js';
|
import { ERROR_CODES, TaskMasterError } from './errors/task-master-error.js';
|
||||||
import type { IConfiguration } from './interfaces/configuration.interface.js';
|
import type { IConfiguration } from './interfaces/configuration.interface.js';
|
||||||
import type { Task, TaskStatus, TaskFilter } from './types/index.js';
|
import type { Task, TaskStatus, TaskFilter } from './types/index.js';
|
||||||
@@ -33,7 +37,10 @@ export class TaskMasterCore {
|
|||||||
|
|
||||||
constructor(options: TaskMasterCoreOptions) {
|
constructor(options: TaskMasterCoreOptions) {
|
||||||
if (!options.projectPath) {
|
if (!options.projectPath) {
|
||||||
throw new TaskMasterError('Project path is required', ERROR_CODES.MISSING_CONFIGURATION);
|
throw new TaskMasterError(
|
||||||
|
'Project path is required',
|
||||||
|
ERROR_CODES.MISSING_CONFIGURATION
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create config manager
|
// Create config manager
|
||||||
@@ -108,7 +115,10 @@ export class TaskMasterCore {
|
|||||||
/**
|
/**
|
||||||
* Get tasks by status
|
* Get tasks by status
|
||||||
*/
|
*/
|
||||||
async getTasksByStatus(status: TaskStatus | TaskStatus[], tag?: string): Promise<Task[]> {
|
async getTasksByStatus(
|
||||||
|
status: TaskStatus | TaskStatus[],
|
||||||
|
tag?: string
|
||||||
|
): Promise<Task[]> {
|
||||||
await this.ensureInitialized();
|
await this.ensureInitialized();
|
||||||
return this.taskService.getTasksByStatus(status, tag);
|
return this.taskService.getTasksByStatus(status, tag);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,7 +103,10 @@ export interface TaskCollection {
|
|||||||
/**
|
/**
|
||||||
* Type for creating a new task (without generated fields)
|
* 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'>[];
|
subtasks?: Omit<Subtask, 'id' | 'parentId' | 'createdAt' | 'updatedAt'>[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -145,7 +148,15 @@ export interface TaskSortOptions {
|
|||||||
export function isTaskStatus(value: unknown): value is TaskStatus {
|
export function isTaskStatus(value: unknown): value is TaskStatus {
|
||||||
return (
|
return (
|
||||||
typeof value === 'string' &&
|
typeof value === 'string' &&
|
||||||
['pending', 'in-progress', 'done', 'deferred', 'cancelled', 'blocked', 'review'].includes(value)
|
[
|
||||||
|
'pending',
|
||||||
|
'in-progress',
|
||||||
|
'done',
|
||||||
|
'deferred',
|
||||||
|
'cancelled',
|
||||||
|
'blocked',
|
||||||
|
'review'
|
||||||
|
].includes(value)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,7 +164,10 @@ export function isTaskStatus(value: unknown): value is TaskStatus {
|
|||||||
* Type guard to check if a value is a valid TaskPriority
|
* Type guard to check if a value is a valid TaskPriority
|
||||||
*/
|
*/
|
||||||
export function isTaskPriority(value: unknown): value is 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)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -161,7 +175,8 @@ export function isTaskPriority(value: unknown): value is TaskPriority {
|
|||||||
*/
|
*/
|
||||||
export function isTaskComplexity(value: unknown): value is TaskComplexity {
|
export function isTaskComplexity(value: unknown): value is TaskComplexity {
|
||||||
return (
|
return (
|
||||||
typeof value === 'string' && ['simple', 'moderate', 'complex', 'very-complex'].includes(value)
|
typeof value === 'string' &&
|
||||||
|
['simple', 'moderate', 'complex', 'very-complex'].includes(value)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,9 +33,14 @@ export function generateTaskId(): string {
|
|||||||
* // Returns: "TASK-123-A7B3.2"
|
* // 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
|
// 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
|
// Extract sequential numbers and find the highest
|
||||||
const sequentialNumbers = parentSubtasks
|
const sequentialNumbers = parentSubtasks
|
||||||
@@ -48,7 +53,8 @@ export function generateSubtaskId(parentId: string, existingSubtasks: string[] =
|
|||||||
.sort((a, b) => a - b);
|
.sort((a, b) => a - b);
|
||||||
|
|
||||||
// Determine the next sequential number
|
// 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}`;
|
return `${parentId}.${nextSequential}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -323,7 +323,9 @@ describe('TaskMasterCore - listTasks E2E', () => {
|
|||||||
|
|
||||||
it('should validate task entities', async () => {
|
it('should validate task entities', async () => {
|
||||||
// Write invalid task data
|
// Write invalid task data
|
||||||
const invalidDir = await fs.mkdtemp(path.join(os.tmpdir(), 'tm-invalid-'));
|
const invalidDir = await fs.mkdtemp(
|
||||||
|
path.join(os.tmpdir(), 'tm-invalid-')
|
||||||
|
);
|
||||||
const tasksDir = path.join(invalidDir, '.taskmaster', 'tasks');
|
const tasksDir = path.join(invalidDir, '.taskmaster', 'tasks');
|
||||||
await fs.mkdir(tasksDir, { recursive: true });
|
await fs.mkdir(tasksDir, { recursive: true });
|
||||||
|
|
||||||
@@ -349,7 +351,10 @@ describe('TaskMasterCore - listTasks E2E', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
await fs.writeFile(path.join(tasksDir, 'tasks.json'), JSON.stringify(invalidData));
|
await fs.writeFile(
|
||||||
|
path.join(tasksDir, 'tasks.json'),
|
||||||
|
JSON.stringify(invalidData)
|
||||||
|
);
|
||||||
|
|
||||||
const invalidCore = createTaskMasterCore(invalidDir);
|
const invalidCore = createTaskMasterCore(invalidDir);
|
||||||
|
|
||||||
@@ -379,7 +384,12 @@ describe('TaskMasterCore - listTasks E2E', () => {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const tagFile = path.join(tmpDir, '.taskmaster', 'tasks', 'feature-branch.json');
|
const tagFile = path.join(
|
||||||
|
tmpDir,
|
||||||
|
'.taskmaster',
|
||||||
|
'tasks',
|
||||||
|
'feature-branch.json'
|
||||||
|
);
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
tagFile,
|
tagFile,
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
|
|||||||
@@ -58,7 +58,10 @@ export class MockProvider extends BaseProvider {
|
|||||||
throw new Error('Mock provider error');
|
throw new Error('Mock provider error');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.options.failAfterAttempts && this.attemptCount <= this.options.failAfterAttempts) {
|
if (
|
||||||
|
this.options.failAfterAttempts &&
|
||||||
|
this.attemptCount <= this.options.failAfterAttempts
|
||||||
|
) {
|
||||||
if (this.options.simulateRateLimit) {
|
if (this.options.simulateRateLimit) {
|
||||||
throw new Error('Rate limit exceeded - too many requests (429)');
|
throw new Error('Rate limit exceeded - too many requests (429)');
|
||||||
}
|
}
|
||||||
@@ -200,6 +203,8 @@ export class MockProvider extends BaseProvider {
|
|||||||
|
|
||||||
// Override retry configuration for testing
|
// Override retry configuration for testing
|
||||||
protected getMaxRetries(): number {
|
protected getMaxRetries(): number {
|
||||||
return this.options.failAfterAttempts ? this.options.failAfterAttempts + 1 : 3;
|
return this.options.failAfterAttempts
|
||||||
|
? this.options.failAfterAttempts + 1
|
||||||
|
: 3;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { beforeEach, describe, expect, it } from 'vitest';
|
import { beforeEach, describe, expect, it } from 'vitest';
|
||||||
import { ERROR_CODES, TaskMasterError } from '../../src/errors/task-master-error';
|
import {
|
||||||
|
ERROR_CODES,
|
||||||
|
TaskMasterError
|
||||||
|
} from '../../src/errors/task-master-error';
|
||||||
import { MockProvider } from '../mocks/mock-provider';
|
import { MockProvider } from '../mocks/mock-provider';
|
||||||
|
|
||||||
describe('BaseProvider', () => {
|
describe('BaseProvider', () => {
|
||||||
@@ -64,21 +67,21 @@ describe('BaseProvider', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should validate temperature range', async () => {
|
it('should validate temperature range', async () => {
|
||||||
await expect(provider.generateCompletion('Test', { temperature: 3 })).rejects.toThrow(
|
await expect(
|
||||||
'Temperature must be between 0 and 2'
|
provider.generateCompletion('Test', { temperature: 3 })
|
||||||
);
|
).rejects.toThrow('Temperature must be between 0 and 2');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should validate maxTokens range', async () => {
|
it('should validate maxTokens range', async () => {
|
||||||
await expect(provider.generateCompletion('Test', { maxTokens: 0 })).rejects.toThrow(
|
await expect(
|
||||||
'Max tokens must be between 1 and 100000'
|
provider.generateCompletion('Test', { maxTokens: 0 })
|
||||||
);
|
).rejects.toThrow('Max tokens must be between 1 and 100000');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should validate topP range', async () => {
|
it('should validate topP range', async () => {
|
||||||
await expect(provider.generateCompletion('Test', { topP: 1.5 })).rejects.toThrow(
|
await expect(
|
||||||
'Top-p must be between 0 and 1'
|
provider.generateCompletion('Test', { topP: 1.5 })
|
||||||
);
|
).rejects.toThrow('Top-p must be between 0 and 1');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -125,7 +128,9 @@ describe('BaseProvider', () => {
|
|||||||
const provider = new MockProvider({ apiKey: 'test-key' });
|
const provider = new MockProvider({ apiKey: 'test-key' });
|
||||||
|
|
||||||
// Access protected method through type assertion
|
// Access protected method through type assertion
|
||||||
const calculateDelay = (provider as any).calculateBackoffDelay.bind(provider);
|
const calculateDelay = (provider as any).calculateBackoffDelay.bind(
|
||||||
|
provider
|
||||||
|
);
|
||||||
|
|
||||||
const delay1 = calculateDelay(1);
|
const delay1 = calculateDelay(1);
|
||||||
const delay2 = calculateDelay(2);
|
const delay2 = calculateDelay(2);
|
||||||
@@ -164,7 +169,9 @@ describe('BaseProvider', () => {
|
|||||||
|
|
||||||
it('should identify rate limit errors correctly', () => {
|
it('should identify rate limit errors correctly', () => {
|
||||||
const provider = new MockProvider({ apiKey: 'test-key' });
|
const provider = new MockProvider({ apiKey: 'test-key' });
|
||||||
const isRateLimitError = (provider as any).isRateLimitError.bind(provider);
|
const isRateLimitError = (provider as any).isRateLimitError.bind(
|
||||||
|
provider
|
||||||
|
);
|
||||||
|
|
||||||
expect(isRateLimitError(new Error('Rate limit exceeded'))).toBe(true);
|
expect(isRateLimitError(new Error('Rate limit exceeded'))).toBe(true);
|
||||||
expect(isRateLimitError(new Error('Too many requests'))).toBe(true);
|
expect(isRateLimitError(new Error('Too many requests'))).toBe(true);
|
||||||
|
|||||||
@@ -16,7 +16,12 @@ import {
|
|||||||
version
|
version
|
||||||
} from '@tm/core';
|
} from '@tm/core';
|
||||||
|
|
||||||
import type { PlaceholderTask, TaskId, TaskPriority, TaskStatus } from '@tm/core';
|
import type {
|
||||||
|
PlaceholderTask,
|
||||||
|
TaskId,
|
||||||
|
TaskPriority,
|
||||||
|
TaskStatus
|
||||||
|
} from '@tm/core';
|
||||||
|
|
||||||
describe('tm-core smoke tests', () => {
|
describe('tm-core smoke tests', () => {
|
||||||
describe('package metadata', () => {
|
describe('package metadata', () => {
|
||||||
@@ -45,7 +50,6 @@ describe('tm-core smoke tests', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
describe('placeholder storage', () => {
|
describe('placeholder storage', () => {
|
||||||
it('should perform basic storage operations', async () => {
|
it('should perform basic storage operations', async () => {
|
||||||
const storage = new PlaceholderStorage();
|
const storage = new PlaceholderStorage();
|
||||||
|
|||||||
@@ -5,7 +5,12 @@ export default defineConfig({
|
|||||||
test: {
|
test: {
|
||||||
globals: true,
|
globals: true,
|
||||||
environment: 'node',
|
environment: 'node',
|
||||||
include: ['tests/**/*.test.ts', 'tests/**/*.spec.ts', 'src/**/*.test.ts', 'src/**/*.spec.ts'],
|
include: [
|
||||||
|
'tests/**/*.test.ts',
|
||||||
|
'tests/**/*.spec.ts',
|
||||||
|
'src/**/*.test.ts',
|
||||||
|
'src/**/*.spec.ts'
|
||||||
|
],
|
||||||
exclude: ['node_modules', 'dist', '.git', '.cache'],
|
exclude: ['node_modules', 'dist', '.git', '.cache'],
|
||||||
coverage: {
|
coverage: {
|
||||||
provider: 'v8',
|
provider: 'v8',
|
||||||
|
|||||||
Reference in New Issue
Block a user