chore: add tm-core package with tag and tasks.json
This commit is contained in:
45
packages/tm-core/.eslintrc.cjs
Normal file
45
packages/tm-core/.eslintrc.cjs
Normal file
@@ -0,0 +1,45 @@
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
es2022: true
|
||||
},
|
||||
extends: ['eslint:recommended'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: 'module'
|
||||
},
|
||||
plugins: ['@typescript-eslint'],
|
||||
rules: {
|
||||
// General code quality
|
||||
'no-console': 'warn',
|
||||
'prefer-const': 'error',
|
||||
'no-var': 'error',
|
||||
'object-shorthand': 'error',
|
||||
'prefer-template': 'error',
|
||||
'no-duplicate-imports': 'error',
|
||||
|
||||
// TypeScript specific rules (basic)
|
||||
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }]
|
||||
},
|
||||
ignorePatterns: [
|
||||
'dist/',
|
||||
'node_modules/',
|
||||
'coverage/',
|
||||
'*.js',
|
||||
'!.eslintrc.cjs'
|
||||
],
|
||||
overrides: [
|
||||
{
|
||||
files: ['**/*.test.ts', '**/*.spec.ts'],
|
||||
env: {
|
||||
jest: true
|
||||
},
|
||||
rules: {
|
||||
'no-console': 'off'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
83
packages/tm-core/.gitignore
vendored
Normal file
83
packages/tm-core/.gitignore
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
*.pnp
|
||||
.pnp.js
|
||||
|
||||
# Build output
|
||||
dist/
|
||||
build/
|
||||
*.tsbuildinfo
|
||||
|
||||
# Coverage reports
|
||||
coverage/
|
||||
*.lcov
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Dependency directories
|
||||
jspm_packages/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
14
packages/tm-core/.prettierrc.json
Normal file
14
packages/tm-core/.prettierrc.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"semi": true,
|
||||
"trailingComma": "es5",
|
||||
"singleQuote": true,
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "avoid",
|
||||
"endOfLine": "lf",
|
||||
"quoteProps": "as-needed",
|
||||
"bracketSameLine": false,
|
||||
"proseWrap": "preserve"
|
||||
}
|
||||
70
packages/tm-core/CHANGELOG.md
Normal file
70
packages/tm-core/CHANGELOG.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to the @task-master/tm-core package will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- Initial package structure and configuration
|
||||
- TypeScript support with strict mode
|
||||
- Dual ESM/CJS build system with tsup
|
||||
- Jest testing framework with TypeScript support
|
||||
- ESLint and Prettier for code quality
|
||||
- Modular architecture with barrel exports
|
||||
- Placeholder implementations for all modules
|
||||
- Comprehensive documentation and README
|
||||
|
||||
### Development Infrastructure
|
||||
- tsup configuration for dual format builds
|
||||
- Jest configuration with ESM support
|
||||
- ESLint configuration with TypeScript rules
|
||||
- Prettier configuration for consistent formatting
|
||||
- Complete package.json with all required fields
|
||||
- TypeScript configuration with strict settings
|
||||
- .gitignore for development files
|
||||
|
||||
### Package Structure
|
||||
- `src/types/` - TypeScript type definitions (placeholder)
|
||||
- `src/providers/` - AI provider implementations (placeholder)
|
||||
- `src/storage/` - Storage layer abstractions (placeholder)
|
||||
- `src/parser/` - Task parsing utilities (placeholder)
|
||||
- `src/utils/` - Common utility functions (placeholder)
|
||||
- `src/errors/` - Custom error classes (placeholder)
|
||||
- `tests/` - Test directories and setup
|
||||
|
||||
## [1.0.0] - TBD
|
||||
|
||||
### Planned Features
|
||||
- Complete TypeScript type system
|
||||
- AI provider implementations
|
||||
- Storage adapters
|
||||
- Task parsing capabilities
|
||||
- Comprehensive utility functions
|
||||
- Custom error handling
|
||||
- Full test coverage
|
||||
- Complete documentation
|
||||
|
||||
---
|
||||
|
||||
## Release Notes
|
||||
|
||||
### Version 1.0.0 (Coming Soon)
|
||||
This will be the first stable release of tm-core with complete implementations of all modules. Currently, all modules contain placeholder implementations to establish the package structure and enable development of dependent packages.
|
||||
|
||||
### Development Status
|
||||
- ✅ Package structure and configuration
|
||||
- ✅ Build and test infrastructure
|
||||
- ✅ Development tooling setup
|
||||
- 🚧 TypeScript types implementation (Task 116)
|
||||
- 🚧 AI provider system (Task 117)
|
||||
- 🚧 Storage layer (Task 118)
|
||||
- 🚧 Task parser (Task 119)
|
||||
- 🚧 Utility functions (Task 120)
|
||||
- 🚧 Error handling (Task 121)
|
||||
- 🚧 Configuration system (Task 122)
|
||||
- 🚧 Testing infrastructure (Task 123)
|
||||
- 🚧 Documentation (Task 124)
|
||||
- 🚧 Package finalization (Task 125)
|
||||
226
packages/tm-core/README.md
Normal file
226
packages/tm-core/README.md
Normal file
@@ -0,0 +1,226 @@
|
||||
# @task-master/tm-core
|
||||
|
||||
Core library for Task Master AI - providing task management and orchestration capabilities with TypeScript support.
|
||||
|
||||
## Overview
|
||||
|
||||
`tm-core` is the foundational library that powers Task Master AI's task management system. It provides a comprehensive set of tools for creating, managing, and orchestrating tasks with AI integration.
|
||||
|
||||
## Features
|
||||
|
||||
- **TypeScript-first**: Built with full TypeScript support and strict type checking
|
||||
- **Dual Format**: Supports both ESM and CommonJS with automatic format detection
|
||||
- **Modular Architecture**: Clean separation of concerns with dedicated modules for different functionality
|
||||
- **AI Provider Integration**: Pluggable AI provider system for task generation and management
|
||||
- **Flexible Storage**: Abstracted storage layer supporting different persistence strategies
|
||||
- **Task Parsing**: Advanced parsing capabilities for various task definition formats
|
||||
- **Error Handling**: Comprehensive error system with specific error types
|
||||
- **Testing**: Complete test coverage with Jest and TypeScript support
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @task-master/tm-core
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```typescript
|
||||
import { generateTaskId, PlaceholderTask } from '@task-master/tm-core';
|
||||
|
||||
// Generate a unique task ID
|
||||
const taskId = generateTaskId();
|
||||
|
||||
// Create a task (coming soon - full implementation)
|
||||
const task: PlaceholderTask = {
|
||||
id: taskId,
|
||||
title: 'My Task',
|
||||
status: 'pending',
|
||||
priority: 'medium'
|
||||
};
|
||||
```
|
||||
|
||||
### Modular Imports
|
||||
|
||||
You can import specific modules to reduce bundle size:
|
||||
|
||||
```typescript
|
||||
// Import types only
|
||||
import type { TaskId, TaskStatus } from '@task-master/tm-core/types';
|
||||
|
||||
// Import utilities
|
||||
import { generateTaskId, formatDate } from '@task-master/tm-core/utils';
|
||||
|
||||
// Import providers
|
||||
import { PlaceholderProvider } from '@task-master/tm-core/providers';
|
||||
|
||||
// Import storage
|
||||
import { PlaceholderStorage } from '@task-master/tm-core/storage';
|
||||
|
||||
// Import parsers
|
||||
import { PlaceholderParser } from '@task-master/tm-core/parser';
|
||||
|
||||
// Import errors
|
||||
import { TmCoreError, TaskNotFoundError } from '@task-master/tm-core/errors';
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
The library is organized into several key modules:
|
||||
|
||||
- **types/**: TypeScript type definitions and interfaces
|
||||
- **providers/**: AI provider implementations for task generation
|
||||
- **storage/**: Storage adapters for different persistence strategies
|
||||
- **parser/**: Task parsing utilities for various formats
|
||||
- **utils/**: Common utility functions and helpers
|
||||
- **errors/**: Custom error classes and error handling
|
||||
|
||||
## Development
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js >= 18.0.0
|
||||
- npm or yarn
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Build the library
|
||||
npm run build
|
||||
|
||||
# Run tests
|
||||
npm test
|
||||
|
||||
# Run tests with coverage
|
||||
npm run test:coverage
|
||||
|
||||
# Lint code
|
||||
npm run lint
|
||||
|
||||
# Format code
|
||||
npm run format
|
||||
```
|
||||
|
||||
### Scripts
|
||||
|
||||
- `build`: Build the library for both ESM and CJS formats
|
||||
- `build:watch`: Build in watch mode for development
|
||||
- `test`: Run the test suite
|
||||
- `test:watch`: Run tests in watch mode
|
||||
- `test:coverage`: Run tests with coverage reporting
|
||||
- `lint`: Lint TypeScript files
|
||||
- `lint:fix`: Lint and auto-fix issues
|
||||
- `format`: Format code with Prettier
|
||||
- `format:check`: Check code formatting
|
||||
- `typecheck`: Type-check without emitting files
|
||||
- `clean`: Clean build artifacts
|
||||
- `dev`: Development mode with watch
|
||||
|
||||
## ESM and CommonJS Support
|
||||
|
||||
This package supports both ESM and CommonJS formats automatically:
|
||||
|
||||
```javascript
|
||||
// ESM
|
||||
import { generateTaskId } from '@task-master/tm-core';
|
||||
|
||||
// CommonJS
|
||||
const { generateTaskId } = require('@task-master/tm-core');
|
||||
```
|
||||
|
||||
## Roadmap
|
||||
|
||||
This is the initial package structure. The following features are planned for implementation:
|
||||
|
||||
### Task 116: TypeScript Types
|
||||
- [ ] Complete type definitions for tasks, projects, and configurations
|
||||
- [ ] Zod schema validation
|
||||
- [ ] Generic type utilities
|
||||
|
||||
### Task 117: AI Provider System
|
||||
- [ ] Base provider interface
|
||||
- [ ] Anthropic Claude integration
|
||||
- [ ] OpenAI integration
|
||||
- [ ] Perplexity integration
|
||||
- [ ] Provider factory and registry
|
||||
|
||||
### Task 118: Storage Layer
|
||||
- [ ] File system storage adapter
|
||||
- [ ] Memory storage adapter
|
||||
- [ ] Storage interface and factory
|
||||
|
||||
### Task 119: Task Parser
|
||||
- [ ] PRD parser implementation
|
||||
- [ ] Markdown parser
|
||||
- [ ] JSON task format parser
|
||||
- [ ] Validation utilities
|
||||
|
||||
### Task 120: Utility Functions
|
||||
- [ ] Task ID generation
|
||||
- [ ] Date formatting
|
||||
- [ ] Validation helpers
|
||||
- [ ] File system utilities
|
||||
|
||||
### Task 121: Error Handling
|
||||
- [ ] Task-specific errors
|
||||
- [ ] Storage errors
|
||||
- [ ] Provider errors
|
||||
- [ ] Validation errors
|
||||
|
||||
### Task 122: Configuration System
|
||||
- [ ] Configuration schema
|
||||
- [ ] Default configurations
|
||||
- [ ] Environment variable support
|
||||
|
||||
### Task 123: Testing Infrastructure
|
||||
- [ ] Unit test coverage
|
||||
- [ ] Integration tests
|
||||
- [ ] Mock utilities
|
||||
|
||||
### Task 124: Documentation
|
||||
- [ ] API documentation
|
||||
- [ ] Usage examples
|
||||
- [ ] Migration guides
|
||||
|
||||
### Task 125: Package Finalization
|
||||
- [ ] Final testing and validation
|
||||
- [ ] Release preparation
|
||||
- [ ] CI/CD integration
|
||||
|
||||
## Implementation Checklist
|
||||
|
||||
### ✅ Task 115: Initialize tm-core Package Structure (COMPLETED)
|
||||
- [x] Create tm-core directory structure and base configuration files
|
||||
- [x] Configure build and test infrastructure
|
||||
- [x] Create barrel export files for all directories
|
||||
- [x] Add development tooling and documentation
|
||||
- [x] Validate package structure and prepare for development
|
||||
|
||||
### 🚧 Remaining Implementation Tasks
|
||||
- [ ] **Task 116**: TypeScript Types - Complete type definitions for tasks, projects, and configurations
|
||||
- [ ] **Task 117**: AI Provider System - Base provider interface and integrations
|
||||
- [ ] **Task 118**: Storage Layer - File system and memory storage adapters
|
||||
- [ ] **Task 119**: Task Parser - PRD, Markdown, and JSON parsers
|
||||
- [ ] **Task 120**: Utility Functions - Task ID generation, validation helpers
|
||||
- [ ] **Task 121**: Error Handling - Task-specific and validation errors
|
||||
- [ ] **Task 122**: Configuration System - Schema and environment support
|
||||
- [ ] **Task 123**: Testing Infrastructure - Complete unit and integration tests
|
||||
- [ ] **Task 124**: Documentation - API docs and usage examples
|
||||
- [ ] **Task 125**: Package Finalization - Release preparation and CI/CD
|
||||
|
||||
## Contributing
|
||||
|
||||
This package is part of the Task Master AI project. Please refer to the main project's contributing guidelines.
|
||||
|
||||
## License
|
||||
|
||||
MIT - See the main project's LICENSE file for details.
|
||||
|
||||
## Support
|
||||
|
||||
For questions and support, please refer to the main Task Master AI documentation.
|
||||
50
packages/tm-core/jest.config.js
Normal file
50
packages/tm-core/jest.config.js
Normal file
@@ -0,0 +1,50 @@
|
||||
/** @type {import('jest').Config} */
|
||||
export default {
|
||||
preset: 'ts-jest/presets/default-esm',
|
||||
extensionsToTreatAsEsm: ['.ts'],
|
||||
testEnvironment: 'node',
|
||||
roots: ['<rootDir>/src', '<rootDir>/tests'],
|
||||
testMatch: [
|
||||
'**/tests/**/*.test.ts',
|
||||
'**/tests/**/*.spec.ts',
|
||||
'**/__tests__/**/*.ts',
|
||||
'**/?(*.)+(spec|test).ts'
|
||||
],
|
||||
transform: {
|
||||
'^.+\\.ts$': [
|
||||
'ts-jest',
|
||||
{
|
||||
useESM: true,
|
||||
tsconfig: {
|
||||
module: 'ESNext',
|
||||
target: 'ES2022'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
moduleNameMapper: {
|
||||
'^@/(.*)$': '<rootDir>/src/$1',
|
||||
'^@/types/(.*)$': '<rootDir>/src/types/$1',
|
||||
'^@/providers/(.*)$': '<rootDir>/src/providers/$1',
|
||||
'^@/storage/(.*)$': '<rootDir>/src/storage/$1',
|
||||
'^@/parser/(.*)$': '<rootDir>/src/parser/$1',
|
||||
'^@/utils/(.*)$': '<rootDir>/src/utils/$1',
|
||||
'^@/errors/(.*)$': '<rootDir>/src/errors/$1'
|
||||
},
|
||||
collectCoverageFrom: ['src/**/*.ts', '!src/**/*.d.ts', '!src/**/index.ts'],
|
||||
coverageDirectory: 'coverage',
|
||||
coverageReporters: ['text', 'lcov', 'html'],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
branches: 80,
|
||||
functions: 80,
|
||||
lines: 80,
|
||||
statements: 80
|
||||
}
|
||||
},
|
||||
setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
|
||||
testTimeout: 10000,
|
||||
verbose: true,
|
||||
clearMocks: true,
|
||||
restoreMocks: true
|
||||
};
|
||||
7021
packages/tm-core/package-lock.json
generated
Normal file
7021
packages/tm-core/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
79
packages/tm-core/package.json
Normal file
79
packages/tm-core/package.json
Normal file
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"name": "@task-master/tm-core",
|
||||
"version": "1.0.0",
|
||||
"description": "Core library for Task Master - TypeScript task management system",
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.cjs"
|
||||
},
|
||||
"./types": {
|
||||
"types": "./dist/types/index.d.ts",
|
||||
"import": "./dist/types/index.js",
|
||||
"require": "./dist/types/index.cjs"
|
||||
},
|
||||
"./providers": {
|
||||
"types": "./dist/providers/index.d.ts",
|
||||
"import": "./dist/providers/index.js",
|
||||
"require": "./dist/providers/index.cjs"
|
||||
},
|
||||
"./storage": {
|
||||
"types": "./dist/storage/index.d.ts",
|
||||
"import": "./dist/storage/index.js",
|
||||
"require": "./dist/storage/index.cjs"
|
||||
},
|
||||
"./parser": {
|
||||
"types": "./dist/parser/index.d.ts",
|
||||
"import": "./dist/parser/index.js",
|
||||
"require": "./dist/parser/index.cjs"
|
||||
},
|
||||
"./utils": {
|
||||
"types": "./dist/utils/index.d.ts",
|
||||
"import": "./dist/utils/index.js",
|
||||
"require": "./dist/utils/index.cjs"
|
||||
},
|
||||
"./errors": {
|
||||
"types": "./dist/errors/index.d.ts",
|
||||
"import": "./dist/errors/index.js",
|
||||
"require": "./dist/errors/index.cjs"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"dev": "tsup --watch",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:coverage": "jest --coverage",
|
||||
"lint": "eslint src --ext .ts",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/node": "^20.11.30",
|
||||
"@typescript-eslint/eslint-plugin": "^7.3.1",
|
||||
"@typescript-eslint/parser": "^7.3.1",
|
||||
"eslint": "^8.57.0",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "^3.2.5",
|
||||
"ts-jest": "^29.1.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsup": "^8.0.2",
|
||||
"typescript": "^5.4.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"files": ["dist", "README.md", "CHANGELOG.md"],
|
||||
"keywords": ["task-management", "typescript", "ai", "prd", "parser"],
|
||||
"author": "Task Master AI",
|
||||
"license": "MIT"
|
||||
}
|
||||
30
packages/tm-core/src/config/config-schema.ts
Normal file
30
packages/tm-core/src/config/config-schema.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* @fileoverview Zod validation schema for IConfiguration interface
|
||||
* This file provides the main schema export for configuration validation
|
||||
*/
|
||||
|
||||
export {
|
||||
configurationSchema,
|
||||
partialConfigurationSchema,
|
||||
modelConfigSchema,
|
||||
providerConfigSchema,
|
||||
taskSettingsSchema,
|
||||
tagSettingsSchema,
|
||||
storageSettingsSchema,
|
||||
retrySettingsSchema,
|
||||
loggingSettingsSchema,
|
||||
loggingConfigSchema, // Legacy alias
|
||||
cacheConfigSchema,
|
||||
securitySettingsSchema,
|
||||
taskPrioritySchema,
|
||||
taskComplexitySchema,
|
||||
logLevelSchema,
|
||||
storageTypeSchema,
|
||||
tagNamingConventionSchema,
|
||||
bufferEncodingSchema,
|
||||
type ConfigurationSchema,
|
||||
type PartialConfigurationSchema
|
||||
} from './validation.js';
|
||||
|
||||
// Re-export the main schema as the default export for convenience
|
||||
export { configurationSchema as default } from './validation.js';
|
||||
238
packages/tm-core/src/config/validation.ts
Normal file
238
packages/tm-core/src/config/validation.ts
Normal file
@@ -0,0 +1,238 @@
|
||||
/**
|
||||
* @fileoverview Zod validation schemas for configuration interfaces
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import { TaskPriority, TaskComplexity } from '../types/index.js';
|
||||
|
||||
// ============================================================================
|
||||
// Enum Schemas
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Task priority validation schema
|
||||
*/
|
||||
export const taskPrioritySchema = z.enum(['low', 'medium', 'high', 'critical']);
|
||||
|
||||
/**
|
||||
* Task complexity validation schema
|
||||
*/
|
||||
export const taskComplexitySchema = z.enum([
|
||||
'simple',
|
||||
'moderate',
|
||||
'complex',
|
||||
'very-complex'
|
||||
]);
|
||||
|
||||
/**
|
||||
* Log level validation schema
|
||||
*/
|
||||
export const logLevelSchema = z.enum(['error', 'warn', 'info', 'debug']);
|
||||
|
||||
/**
|
||||
* Storage type validation schema
|
||||
*/
|
||||
export const storageTypeSchema = z.enum(['file', 'memory', 'database']);
|
||||
|
||||
/**
|
||||
* Tag naming convention validation schema
|
||||
*/
|
||||
export const tagNamingConventionSchema = z.enum([
|
||||
'kebab-case',
|
||||
'camelCase',
|
||||
'snake_case'
|
||||
]);
|
||||
|
||||
/**
|
||||
* Buffer encoding validation schema
|
||||
*/
|
||||
export const bufferEncodingSchema = z.enum([
|
||||
'ascii',
|
||||
'utf8',
|
||||
'utf-8',
|
||||
'utf16le',
|
||||
'ucs2',
|
||||
'ucs-2',
|
||||
'base64',
|
||||
'base64url',
|
||||
'latin1',
|
||||
'binary',
|
||||
'hex'
|
||||
]);
|
||||
|
||||
// ============================================================================
|
||||
// Sub-interface Schemas
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Model configuration validation schema
|
||||
*/
|
||||
export const modelConfigSchema = z.object({
|
||||
main: z.string().min(1, 'Main model name is required'),
|
||||
research: z.string().min(1).optional(),
|
||||
fallback: z.string().min(1, 'Fallback model name is required')
|
||||
});
|
||||
|
||||
/**
|
||||
* Provider configuration validation schema
|
||||
*/
|
||||
export const providerConfigSchema = z.object({
|
||||
name: z.string().min(1, 'Provider name is required'),
|
||||
apiKey: z.string().optional(),
|
||||
baseUrl: z.string().url().optional(),
|
||||
options: z.record(z.unknown()).optional(),
|
||||
enabled: z.boolean().optional().default(true)
|
||||
});
|
||||
|
||||
/**
|
||||
* Task settings validation schema
|
||||
*/
|
||||
export const taskSettingsSchema = z.object({
|
||||
defaultPriority: taskPrioritySchema,
|
||||
defaultComplexity: taskComplexitySchema,
|
||||
maxSubtasks: z.number().int().min(1).max(100),
|
||||
maxConcurrentTasks: z.number().int().min(1).max(50),
|
||||
autoGenerateIds: z.boolean(),
|
||||
taskIdPrefix: z.string().optional(),
|
||||
validateDependencies: z.boolean(),
|
||||
enableTimestamps: z.boolean(),
|
||||
enableEffortTracking: z.boolean()
|
||||
});
|
||||
|
||||
/**
|
||||
* Tag settings validation schema
|
||||
*/
|
||||
export const tagSettingsSchema = z.object({
|
||||
enableTags: z.boolean(),
|
||||
defaultTag: z.string().min(1),
|
||||
maxTagsPerTask: z.number().int().min(1).max(50),
|
||||
autoCreateFromBranch: z.boolean(),
|
||||
tagNamingConvention: tagNamingConventionSchema
|
||||
});
|
||||
|
||||
/**
|
||||
* Storage settings validation schema
|
||||
*/
|
||||
export const storageSettingsSchema = z.object({
|
||||
type: storageTypeSchema,
|
||||
basePath: z.string().optional(),
|
||||
enableBackup: z.boolean(),
|
||||
maxBackups: z.number().int().min(0).max(100),
|
||||
enableCompression: z.boolean(),
|
||||
encoding: bufferEncodingSchema,
|
||||
atomicOperations: z.boolean()
|
||||
});
|
||||
|
||||
/**
|
||||
* Retry settings validation schema
|
||||
*/
|
||||
export const retrySettingsSchema = z.object({
|
||||
retryAttempts: z.number().int().min(0).max(10),
|
||||
retryDelay: z.number().int().min(100).max(60000),
|
||||
maxRetryDelay: z.number().int().min(1000).max(300000),
|
||||
backoffMultiplier: z.number().min(1).max(10),
|
||||
requestTimeout: z.number().int().min(1000).max(600000),
|
||||
retryOnNetworkError: z.boolean(),
|
||||
retryOnRateLimit: z.boolean()
|
||||
});
|
||||
|
||||
/**
|
||||
* Logging settings validation schema
|
||||
*/
|
||||
export const loggingSettingsSchema = z.object({
|
||||
enabled: z.boolean(),
|
||||
level: logLevelSchema,
|
||||
filePath: z.string().optional(),
|
||||
logRequests: z.boolean(),
|
||||
logPerformance: z.boolean(),
|
||||
logStackTraces: z.boolean(),
|
||||
maxFileSize: z.number().min(1).max(1000),
|
||||
maxFiles: z.number().int().min(1).max(100)
|
||||
});
|
||||
|
||||
/**
|
||||
* Security settings validation schema
|
||||
*/
|
||||
export const securitySettingsSchema = z.object({
|
||||
validateApiKeys: z.boolean(),
|
||||
enableRateLimit: z.boolean(),
|
||||
maxRequestsPerMinute: z.number().int().min(1).max(10000),
|
||||
sanitizeInputs: z.boolean(),
|
||||
maxPromptLength: z.number().int().min(100).max(1000000),
|
||||
allowedFileExtensions: z.array(z.string().regex(/^\.[a-zA-Z0-9]+$/)),
|
||||
enableCors: z.boolean()
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Main Configuration Schema
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Base configuration object schema (without refinements)
|
||||
*/
|
||||
const baseConfigurationSchema = z.object({
|
||||
projectPath: z.string().min(1, 'Project path is required'),
|
||||
aiProvider: z.string().min(1, 'AI provider is required'),
|
||||
apiKeys: z.record(z.string()),
|
||||
models: modelConfigSchema,
|
||||
providers: z.record(providerConfigSchema),
|
||||
tasks: taskSettingsSchema,
|
||||
tags: tagSettingsSchema,
|
||||
storage: storageSettingsSchema,
|
||||
retry: retrySettingsSchema,
|
||||
logging: loggingSettingsSchema,
|
||||
security: securitySettingsSchema,
|
||||
custom: z.record(z.unknown()).optional(),
|
||||
version: z.string().min(1, 'Version is required'),
|
||||
lastUpdated: z.string().min(1, 'Last updated timestamp is required')
|
||||
});
|
||||
|
||||
/**
|
||||
* Main configuration validation schema with custom refinements
|
||||
*/
|
||||
export const configurationSchema = baseConfigurationSchema.refine(
|
||||
(data) => {
|
||||
// Custom validation: maxRetryDelay should be >= retryDelay
|
||||
return data.retry.maxRetryDelay >= data.retry.retryDelay;
|
||||
},
|
||||
{
|
||||
message: 'maxRetryDelay must be greater than or equal to retryDelay',
|
||||
path: ['retry', 'maxRetryDelay']
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Partial configuration validation schema for updates
|
||||
*/
|
||||
export const partialConfigurationSchema = baseConfigurationSchema.partial();
|
||||
|
||||
// ============================================================================
|
||||
// Legacy/Alias Exports (for backwards compatibility)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Alias for loggingSettingsSchema (for backwards compatibility)
|
||||
* @deprecated Use loggingSettingsSchema instead
|
||||
*/
|
||||
export const loggingConfigSchema = loggingSettingsSchema;
|
||||
|
||||
/**
|
||||
* Cache configuration validation schema (stub - not implemented in IConfiguration yet)
|
||||
* This is exported for consistency with config-schema.ts exports
|
||||
*/
|
||||
export const cacheConfigSchema = z
|
||||
.object({
|
||||
enabled: z.boolean().optional().default(false),
|
||||
ttl: z.number().int().min(1).optional().default(300),
|
||||
maxSize: z.number().int().min(1).optional().default(1000)
|
||||
})
|
||||
.optional();
|
||||
|
||||
// ============================================================================
|
||||
// Type exports for runtime validation
|
||||
// ============================================================================
|
||||
|
||||
export type ConfigurationSchema = z.infer<typeof configurationSchema>;
|
||||
export type PartialConfigurationSchema = z.infer<
|
||||
typeof partialConfigurationSchema
|
||||
>;
|
||||
59
packages/tm-core/src/errors/index.ts
Normal file
59
packages/tm-core/src/errors/index.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* @fileoverview Custom error classes for the tm-core package
|
||||
* This file exports all custom error types and error handling utilities
|
||||
*/
|
||||
|
||||
// Error implementations will be defined here
|
||||
// export * from './task-errors.js';
|
||||
// export * from './storage-errors.js';
|
||||
// export * from './provider-errors.js';
|
||||
// export * from './validation-errors.js';
|
||||
|
||||
// Placeholder exports - these will be implemented in later tasks
|
||||
|
||||
/**
|
||||
* Base error class for all tm-core errors
|
||||
* @deprecated This is a placeholder class that will be properly implemented in later tasks
|
||||
*/
|
||||
export class TmCoreError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public code?: string
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'TmCoreError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Error thrown when a task is not found
|
||||
* @deprecated This is a placeholder class that will be properly implemented in later tasks
|
||||
*/
|
||||
export class TaskNotFoundError extends TmCoreError {
|
||||
constructor(taskId: string) {
|
||||
super(`Task not found: ${taskId}`, 'TASK_NOT_FOUND');
|
||||
this.name = 'TaskNotFoundError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Error thrown when validation fails
|
||||
* @deprecated This is a placeholder class that will be properly implemented in later tasks
|
||||
*/
|
||||
export class ValidationError extends TmCoreError {
|
||||
constructor(message: string) {
|
||||
super(message, 'VALIDATION_ERROR');
|
||||
this.name = 'ValidationError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Error thrown when storage operations fail
|
||||
* @deprecated This is a placeholder class that will be properly implemented in later tasks
|
||||
*/
|
||||
export class StorageError extends TmCoreError {
|
||||
constructor(message: string) {
|
||||
super(message, 'STORAGE_ERROR');
|
||||
this.name = 'StorageError';
|
||||
}
|
||||
}
|
||||
324
packages/tm-core/src/errors/task-master-error.ts
Normal file
324
packages/tm-core/src/errors/task-master-error.ts
Normal file
@@ -0,0 +1,324 @@
|
||||
/**
|
||||
* @fileoverview Base error class for Task Master operations
|
||||
* Provides comprehensive error handling with metadata, context, and serialization support
|
||||
*/
|
||||
|
||||
/**
|
||||
* Error codes used throughout the Task Master system
|
||||
*/
|
||||
export const ERROR_CODES = {
|
||||
// File system errors
|
||||
FILE_NOT_FOUND: 'FILE_NOT_FOUND',
|
||||
FILE_READ_ERROR: 'FILE_READ_ERROR',
|
||||
FILE_WRITE_ERROR: 'FILE_WRITE_ERROR',
|
||||
|
||||
// Parsing errors
|
||||
PARSE_ERROR: 'PARSE_ERROR',
|
||||
JSON_PARSE_ERROR: 'JSON_PARSE_ERROR',
|
||||
YAML_PARSE_ERROR: 'YAML_PARSE_ERROR',
|
||||
|
||||
// Validation errors
|
||||
VALIDATION_ERROR: 'VALIDATION_ERROR',
|
||||
SCHEMA_VALIDATION_ERROR: 'SCHEMA_VALIDATION_ERROR',
|
||||
TYPE_VALIDATION_ERROR: 'TYPE_VALIDATION_ERROR',
|
||||
|
||||
// API and network errors
|
||||
API_ERROR: 'API_ERROR',
|
||||
NETWORK_ERROR: 'NETWORK_ERROR',
|
||||
AUTHENTICATION_ERROR: 'AUTHENTICATION_ERROR',
|
||||
AUTHORIZATION_ERROR: 'AUTHORIZATION_ERROR',
|
||||
|
||||
// Task management errors
|
||||
TASK_NOT_FOUND: 'TASK_NOT_FOUND',
|
||||
TASK_DEPENDENCY_ERROR: 'TASK_DEPENDENCY_ERROR',
|
||||
TASK_STATUS_ERROR: 'TASK_STATUS_ERROR',
|
||||
|
||||
// Storage errors
|
||||
STORAGE_ERROR: 'STORAGE_ERROR',
|
||||
DATABASE_ERROR: 'DATABASE_ERROR',
|
||||
|
||||
// Configuration errors
|
||||
CONFIG_ERROR: 'CONFIG_ERROR',
|
||||
MISSING_CONFIGURATION: 'MISSING_CONFIGURATION',
|
||||
INVALID_CONFIGURATION: 'INVALID_CONFIGURATION',
|
||||
|
||||
// Provider errors
|
||||
PROVIDER_ERROR: 'PROVIDER_ERROR',
|
||||
PROVIDER_NOT_FOUND: 'PROVIDER_NOT_FOUND',
|
||||
PROVIDER_INITIALIZATION_ERROR: 'PROVIDER_INITIALIZATION_ERROR',
|
||||
|
||||
// Generic errors
|
||||
INTERNAL_ERROR: 'INTERNAL_ERROR',
|
||||
UNKNOWN_ERROR: 'UNKNOWN_ERROR'
|
||||
} as const;
|
||||
|
||||
export type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES];
|
||||
|
||||
/**
|
||||
* Error context interface for additional error metadata
|
||||
*/
|
||||
export interface ErrorContext {
|
||||
/** Additional details about the error */
|
||||
details?: any;
|
||||
/** Error timestamp */
|
||||
timestamp?: Date;
|
||||
/** Operation that failed */
|
||||
operation?: string;
|
||||
/** Resource identifier related to the error */
|
||||
resource?: string;
|
||||
/** Stack of operations leading to the error */
|
||||
operationStack?: string[];
|
||||
/** User-safe message for display */
|
||||
userMessage?: string;
|
||||
/** Internal error identifier for debugging */
|
||||
errorId?: string;
|
||||
/** Additional metadata */
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializable error representation
|
||||
*/
|
||||
export interface SerializableError {
|
||||
name: string;
|
||||
message: string;
|
||||
code: string;
|
||||
context: ErrorContext;
|
||||
stack?: string;
|
||||
cause?: SerializableError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base error class for all Task Master operations
|
||||
*
|
||||
* Provides comprehensive error handling with:
|
||||
* - Error codes for programmatic handling
|
||||
* - Rich context and metadata support
|
||||
* - Error chaining with cause property
|
||||
* - Serialization for logging and transport
|
||||
* - Sanitization for user-facing messages
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* try {
|
||||
* // Some operation that might fail
|
||||
* throw new TaskMasterError(
|
||||
* 'Failed to parse task file',
|
||||
* ERROR_CODES.PARSE_ERROR,
|
||||
* {
|
||||
* details: { filename: 'tasks.json', line: 42 },
|
||||
* operation: 'parseTaskFile',
|
||||
* userMessage: 'There was an error reading your task file'
|
||||
* }
|
||||
* );
|
||||
* } catch (error) {
|
||||
* console.error(error.toJSON());
|
||||
* throw new TaskMasterError(
|
||||
* 'Operation failed',
|
||||
* ERROR_CODES.INTERNAL_ERROR,
|
||||
* { operation: 'processTask' },
|
||||
* error
|
||||
* );
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export class TaskMasterError extends Error {
|
||||
/** Error code for programmatic handling */
|
||||
public readonly code: string;
|
||||
|
||||
/** Rich context and metadata */
|
||||
public readonly context: ErrorContext;
|
||||
|
||||
/** Original error that caused this error (for error chaining) */
|
||||
public readonly cause?: Error;
|
||||
|
||||
/** Timestamp when error was created */
|
||||
public readonly timestamp: Date;
|
||||
|
||||
/**
|
||||
* Create a new TaskMasterError
|
||||
*
|
||||
* @param message - Human-readable error message
|
||||
* @param code - Error code from ERROR_CODES
|
||||
* @param context - Additional error context and metadata
|
||||
* @param cause - Original error that caused this error (for chaining)
|
||||
*/
|
||||
constructor(
|
||||
message: string,
|
||||
code: string = ERROR_CODES.UNKNOWN_ERROR,
|
||||
context: ErrorContext = {},
|
||||
cause?: Error
|
||||
) {
|
||||
super(message);
|
||||
|
||||
// Set error name
|
||||
this.name = 'TaskMasterError';
|
||||
|
||||
// Set properties
|
||||
this.code = code;
|
||||
this.cause = cause;
|
||||
this.timestamp = new Date();
|
||||
|
||||
// Merge context with defaults
|
||||
this.context = {
|
||||
timestamp: this.timestamp,
|
||||
...context
|
||||
};
|
||||
|
||||
// Fix prototype chain for proper instanceof checks
|
||||
Object.setPrototypeOf(this, TaskMasterError.prototype);
|
||||
|
||||
// Maintain proper stack trace
|
||||
if (Error.captureStackTrace) {
|
||||
Error.captureStackTrace(this, TaskMasterError);
|
||||
}
|
||||
|
||||
// If we have a cause error, append its stack trace
|
||||
if (cause && cause.stack) {
|
||||
this.stack = this.stack + '\nCaused by: ' + cause.stack;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user-friendly error message
|
||||
* Falls back to the main message if no user message is provided
|
||||
*/
|
||||
public getUserMessage(): string {
|
||||
return this.context.userMessage || this.message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get sanitized error details safe for user display
|
||||
* Removes sensitive information and internal details
|
||||
*/
|
||||
public getSanitizedDetails(): Record<string, any> {
|
||||
const { details, userMessage, resource, operation } = this.context;
|
||||
|
||||
return {
|
||||
code: this.code,
|
||||
message: this.getUserMessage(),
|
||||
...(resource && { resource }),
|
||||
...(operation && { operation }),
|
||||
...(details &&
|
||||
typeof details === 'object' &&
|
||||
!this.containsSensitiveInfo(details) && { details })
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if error details contain potentially sensitive information
|
||||
*/
|
||||
private containsSensitiveInfo(obj: any): boolean {
|
||||
if (typeof obj !== 'object' || obj === null) return false;
|
||||
|
||||
const sensitiveKeys = [
|
||||
'password',
|
||||
'token',
|
||||
'key',
|
||||
'secret',
|
||||
'auth',
|
||||
'credential'
|
||||
];
|
||||
const objString = JSON.stringify(obj).toLowerCase();
|
||||
|
||||
return sensitiveKeys.some((key) => objString.includes(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert error to JSON for serialization
|
||||
* Includes all error information for logging and debugging
|
||||
*/
|
||||
public toJSON(): SerializableError {
|
||||
const result: SerializableError = {
|
||||
name: this.name,
|
||||
message: this.message,
|
||||
code: this.code,
|
||||
context: this.context,
|
||||
stack: this.stack
|
||||
};
|
||||
|
||||
// Include serialized cause if present
|
||||
if (this.cause) {
|
||||
if (this.cause instanceof TaskMasterError) {
|
||||
result.cause = this.cause.toJSON();
|
||||
} else {
|
||||
result.cause = {
|
||||
name: this.cause.name,
|
||||
message: this.cause.message,
|
||||
code: ERROR_CODES.UNKNOWN_ERROR,
|
||||
context: {},
|
||||
stack: this.cause.stack
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert error to string representation
|
||||
* Provides formatted output for logging and debugging
|
||||
*/
|
||||
public toString(): string {
|
||||
let result = `${this.name}[${this.code}]: ${this.message}`;
|
||||
|
||||
if (this.context.operation) {
|
||||
result += ` (operation: ${this.context.operation})`;
|
||||
}
|
||||
|
||||
if (this.context.resource) {
|
||||
result += ` (resource: ${this.context.resource})`;
|
||||
}
|
||||
|
||||
if (this.cause) {
|
||||
result += `\nCaused by: ${this.cause.toString()}`;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this error is of a specific code
|
||||
*/
|
||||
public is(code: string): boolean {
|
||||
return this.code === code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this error or any error in its cause chain is of a specific code
|
||||
*/
|
||||
public hasCode(code: string): boolean {
|
||||
if (this.is(code)) return true;
|
||||
|
||||
if (this.cause instanceof TaskMasterError) {
|
||||
return this.cause.hasCode(code);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new error with additional context
|
||||
*/
|
||||
public withContext(
|
||||
additionalContext: Partial<ErrorContext>
|
||||
): TaskMasterError {
|
||||
return new TaskMasterError(
|
||||
this.message,
|
||||
this.code,
|
||||
{ ...this.context, ...additionalContext },
|
||||
this.cause
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new error wrapping this one as the cause
|
||||
*/
|
||||
public wrap(
|
||||
message: string,
|
||||
code: string = ERROR_CODES.INTERNAL_ERROR,
|
||||
context: ErrorContext = {}
|
||||
): TaskMasterError {
|
||||
return new TaskMasterError(message, code, context, this);
|
||||
}
|
||||
}
|
||||
30
packages/tm-core/src/index.ts
Normal file
30
packages/tm-core/src/index.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* @fileoverview Main entry point for the tm-core package
|
||||
* This file exports all public APIs from the core Task Master library
|
||||
*/
|
||||
|
||||
// Re-export types
|
||||
export type * from './types/index';
|
||||
|
||||
// Re-export interfaces
|
||||
export type * from './interfaces/index';
|
||||
export * from './interfaces/index';
|
||||
|
||||
// Re-export providers
|
||||
export * from './providers/index';
|
||||
|
||||
// Re-export storage
|
||||
export * from './storage/index';
|
||||
|
||||
// Re-export parser
|
||||
export * from './parser/index';
|
||||
|
||||
// Re-export utilities
|
||||
export * from './utils/index';
|
||||
|
||||
// Re-export errors
|
||||
export * from './errors/index';
|
||||
|
||||
// Package metadata
|
||||
export const version = '1.0.0';
|
||||
export const name = '@task-master/tm-core';
|
||||
423
packages/tm-core/src/interfaces/ai-provider.interface.ts
Normal file
423
packages/tm-core/src/interfaces/ai-provider.interface.ts
Normal file
@@ -0,0 +1,423 @@
|
||||
/**
|
||||
* @fileoverview AI Provider interface definitions for the tm-core package
|
||||
* This file defines the contract for all AI provider implementations
|
||||
*/
|
||||
|
||||
/**
|
||||
* Options for AI completion requests
|
||||
*/
|
||||
export interface AIOptions {
|
||||
/** Temperature for response randomness (0.0 to 1.0) */
|
||||
temperature?: number;
|
||||
/** Maximum number of tokens to generate */
|
||||
maxTokens?: number;
|
||||
/** Whether to use streaming responses */
|
||||
stream?: boolean;
|
||||
/** Top-p sampling parameter (0.0 to 1.0) */
|
||||
topP?: number;
|
||||
/** Frequency penalty to reduce repetition (-2.0 to 2.0) */
|
||||
frequencyPenalty?: number;
|
||||
/** Presence penalty to encourage new topics (-2.0 to 2.0) */
|
||||
presencePenalty?: number;
|
||||
/** Stop sequences to halt generation */
|
||||
stop?: string | string[];
|
||||
/** Custom system prompt override */
|
||||
systemPrompt?: string;
|
||||
/** Request timeout in milliseconds */
|
||||
timeout?: number;
|
||||
/** Number of retry attempts on failure */
|
||||
retries?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Response from AI completion request
|
||||
*/
|
||||
export interface AIResponse {
|
||||
/** Generated text content */
|
||||
content: string;
|
||||
/** Token count for the request */
|
||||
inputTokens: number;
|
||||
/** Token count for the response */
|
||||
outputTokens: number;
|
||||
/** Total tokens used */
|
||||
totalTokens: number;
|
||||
/** Cost in USD (if available) */
|
||||
cost?: number;
|
||||
/** Model used for generation */
|
||||
model: string;
|
||||
/** Provider name */
|
||||
provider: string;
|
||||
/** Response timestamp */
|
||||
timestamp: string;
|
||||
/** Request duration in milliseconds */
|
||||
duration: number;
|
||||
/** Whether the response was cached */
|
||||
cached?: boolean;
|
||||
/** Finish reason (completed, length, stop, etc.) */
|
||||
finishReason?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* AI model information
|
||||
*/
|
||||
export interface AIModel {
|
||||
/** Model identifier */
|
||||
id: string;
|
||||
/** Human-readable model name */
|
||||
name: string;
|
||||
/** Model description */
|
||||
description?: string;
|
||||
/** Maximum context length in tokens */
|
||||
contextLength: number;
|
||||
/** Input cost per 1K tokens in USD */
|
||||
inputCostPer1K?: number;
|
||||
/** Output cost per 1K tokens in USD */
|
||||
outputCostPer1K?: number;
|
||||
/** Whether the model supports function calling */
|
||||
supportsFunctions?: boolean;
|
||||
/** Whether the model supports vision/image inputs */
|
||||
supportsVision?: boolean;
|
||||
/** Whether the model supports streaming */
|
||||
supportsStreaming?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider capabilities and metadata
|
||||
*/
|
||||
export interface ProviderInfo {
|
||||
/** Provider name */
|
||||
name: string;
|
||||
/** Provider display name */
|
||||
displayName: string;
|
||||
/** Provider description */
|
||||
description?: string;
|
||||
/** Base API URL */
|
||||
baseUrl?: string;
|
||||
/** Available models */
|
||||
models: AIModel[];
|
||||
/** Default model ID */
|
||||
defaultModel: string;
|
||||
/** Whether the provider requires an API key */
|
||||
requiresApiKey: boolean;
|
||||
/** Supported features */
|
||||
features: {
|
||||
streaming?: boolean;
|
||||
functions?: boolean;
|
||||
vision?: boolean;
|
||||
embeddings?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for AI provider implementations
|
||||
* All AI providers must implement this interface
|
||||
*/
|
||||
export interface IAIProvider {
|
||||
/**
|
||||
* Generate a text completion from a prompt
|
||||
* @param prompt - Input prompt text
|
||||
* @param options - Optional generation parameters
|
||||
* @returns Promise that resolves to AI response
|
||||
*/
|
||||
generateCompletion(prompt: string, options?: AIOptions): Promise<AIResponse>;
|
||||
|
||||
/**
|
||||
* Generate a streaming completion (if supported)
|
||||
* @param prompt - Input prompt text
|
||||
* @param options - Optional generation parameters
|
||||
* @returns AsyncIterator of response chunks
|
||||
*/
|
||||
generateStreamingCompletion(
|
||||
prompt: string,
|
||||
options?: AIOptions
|
||||
): AsyncIterator<Partial<AIResponse>>;
|
||||
|
||||
/**
|
||||
* Calculate token count for given text
|
||||
* @param text - Text to count tokens for
|
||||
* @param model - Optional model to use for counting
|
||||
* @returns Number of tokens
|
||||
*/
|
||||
calculateTokens(text: string, model?: string): number;
|
||||
|
||||
/**
|
||||
* Get the provider name
|
||||
* @returns Provider name string
|
||||
*/
|
||||
getName(): string;
|
||||
|
||||
/**
|
||||
* Get current model being used
|
||||
* @returns Current model ID
|
||||
*/
|
||||
getModel(): string;
|
||||
|
||||
/**
|
||||
* Set the model to use for requests
|
||||
* @param model - Model ID to use
|
||||
*/
|
||||
setModel(model: string): void;
|
||||
|
||||
/**
|
||||
* Get the default model for this provider
|
||||
* @returns Default model ID
|
||||
*/
|
||||
getDefaultModel(): string;
|
||||
|
||||
/**
|
||||
* Check if the provider is available and configured
|
||||
* @returns Promise that resolves to availability status
|
||||
*/
|
||||
isAvailable(): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Get provider information and capabilities
|
||||
* @returns Provider information object
|
||||
*/
|
||||
getProviderInfo(): ProviderInfo;
|
||||
|
||||
/**
|
||||
* Get available models for this provider
|
||||
* @returns Array of available models
|
||||
*/
|
||||
getAvailableModels(): AIModel[];
|
||||
|
||||
/**
|
||||
* Validate API key or credentials
|
||||
* @returns Promise that resolves to validation status
|
||||
*/
|
||||
validateCredentials(): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Get usage statistics if available
|
||||
* @returns Promise that resolves to usage stats or null
|
||||
*/
|
||||
getUsageStats(): Promise<ProviderUsageStats | null>;
|
||||
|
||||
/**
|
||||
* Initialize the provider (set up connections, validate config, etc.)
|
||||
* @returns Promise that resolves when initialization is complete
|
||||
*/
|
||||
initialize(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Clean up and close provider connections
|
||||
* @returns Promise that resolves when cleanup is complete
|
||||
*/
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Usage statistics for a provider
|
||||
*/
|
||||
export interface ProviderUsageStats {
|
||||
/** Total requests made */
|
||||
totalRequests: number;
|
||||
/** Total tokens consumed */
|
||||
totalTokens: number;
|
||||
/** Total cost in USD */
|
||||
totalCost: number;
|
||||
/** Requests today */
|
||||
requestsToday: number;
|
||||
/** Tokens used today */
|
||||
tokensToday: number;
|
||||
/** Cost today */
|
||||
costToday: number;
|
||||
/** Average response time in milliseconds */
|
||||
averageResponseTime: number;
|
||||
/** Success rate (0.0 to 1.0) */
|
||||
successRate: number;
|
||||
/** Last request timestamp */
|
||||
lastRequestAt?: string;
|
||||
/** Rate limit information if available */
|
||||
rateLimits?: {
|
||||
requestsPerMinute: number;
|
||||
tokensPerMinute: number;
|
||||
requestsRemaining: number;
|
||||
tokensRemaining: number;
|
||||
resetTime: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for AI provider instances
|
||||
*/
|
||||
export interface AIProviderConfig {
|
||||
/** API key for the provider */
|
||||
apiKey: string;
|
||||
/** Base URL override */
|
||||
baseUrl?: string;
|
||||
/** Default model to use */
|
||||
model?: string;
|
||||
/** Default generation options */
|
||||
defaultOptions?: AIOptions;
|
||||
/** Request timeout in milliseconds */
|
||||
timeout?: number;
|
||||
/** Maximum retry attempts */
|
||||
maxRetries?: number;
|
||||
/** Custom headers to include in requests */
|
||||
headers?: Record<string, string>;
|
||||
/** Enable request/response logging */
|
||||
enableLogging?: boolean;
|
||||
/** Enable usage tracking */
|
||||
enableUsageTracking?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract base class for AI provider implementations
|
||||
* Provides common functionality and enforces the interface
|
||||
*/
|
||||
export abstract class BaseAIProvider implements IAIProvider {
|
||||
protected config: AIProviderConfig;
|
||||
protected currentModel: string;
|
||||
protected usageStats: ProviderUsageStats | null = null;
|
||||
|
||||
constructor(config: AIProviderConfig) {
|
||||
this.config = config;
|
||||
this.currentModel = config.model || this.getDefaultModel();
|
||||
|
||||
if (config.enableUsageTracking) {
|
||||
this.initializeUsageTracking();
|
||||
}
|
||||
}
|
||||
|
||||
// Abstract methods that must be implemented by concrete classes
|
||||
abstract generateCompletion(
|
||||
prompt: string,
|
||||
options?: AIOptions
|
||||
): Promise<AIResponse>;
|
||||
abstract generateStreamingCompletion(
|
||||
prompt: string,
|
||||
options?: AIOptions
|
||||
): AsyncIterator<Partial<AIResponse>>;
|
||||
abstract calculateTokens(text: string, model?: string): number;
|
||||
abstract getName(): string;
|
||||
abstract getDefaultModel(): string;
|
||||
abstract isAvailable(): Promise<boolean>;
|
||||
abstract getProviderInfo(): ProviderInfo;
|
||||
abstract validateCredentials(): Promise<boolean>;
|
||||
abstract initialize(): Promise<void>;
|
||||
abstract close(): Promise<void>;
|
||||
|
||||
// Implemented methods with common functionality
|
||||
getModel(): string {
|
||||
return this.currentModel;
|
||||
}
|
||||
|
||||
setModel(model: string): void {
|
||||
const availableModels = this.getAvailableModels();
|
||||
const modelExists = availableModels.some((m) => m.id === model);
|
||||
|
||||
if (!modelExists) {
|
||||
throw new Error(
|
||||
`Model "${model}" is not available for provider "${this.getName()}"`
|
||||
);
|
||||
}
|
||||
|
||||
this.currentModel = model;
|
||||
}
|
||||
|
||||
getAvailableModels(): AIModel[] {
|
||||
return this.getProviderInfo().models;
|
||||
}
|
||||
|
||||
async getUsageStats(): Promise<ProviderUsageStats | null> {
|
||||
return this.usageStats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize usage tracking
|
||||
*/
|
||||
protected initializeUsageTracking(): void {
|
||||
this.usageStats = {
|
||||
totalRequests: 0,
|
||||
totalTokens: 0,
|
||||
totalCost: 0,
|
||||
requestsToday: 0,
|
||||
tokensToday: 0,
|
||||
costToday: 0,
|
||||
averageResponseTime: 0,
|
||||
successRate: 1.0
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update usage statistics after a request
|
||||
* @param response - AI response to record
|
||||
* @param duration - Request duration in milliseconds
|
||||
* @param success - Whether the request was successful
|
||||
*/
|
||||
protected updateUsageStats(
|
||||
response: AIResponse,
|
||||
duration: number,
|
||||
success: boolean
|
||||
): void {
|
||||
if (!this.usageStats) return;
|
||||
|
||||
this.usageStats.totalRequests++;
|
||||
this.usageStats.totalTokens += response.totalTokens;
|
||||
|
||||
if (response.cost) {
|
||||
this.usageStats.totalCost += response.cost;
|
||||
}
|
||||
|
||||
// Update daily stats (simplified - would need proper date tracking)
|
||||
this.usageStats.requestsToday++;
|
||||
this.usageStats.tokensToday += response.totalTokens;
|
||||
|
||||
if (response.cost) {
|
||||
this.usageStats.costToday += response.cost;
|
||||
}
|
||||
|
||||
// Update average response time
|
||||
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.lastRequestAt = new Date().toISOString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge user options with default options
|
||||
* @param userOptions - User-provided options
|
||||
* @returns Merged options object
|
||||
*/
|
||||
protected mergeOptions(userOptions?: AIOptions): AIOptions {
|
||||
return {
|
||||
temperature: 0.7,
|
||||
maxTokens: 2000,
|
||||
stream: false,
|
||||
topP: 1.0,
|
||||
frequencyPenalty: 0.0,
|
||||
presencePenalty: 0.0,
|
||||
timeout: 30000,
|
||||
retries: 3,
|
||||
...this.config.defaultOptions,
|
||||
...userOptions
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate prompt input
|
||||
* @param prompt - Prompt to validate
|
||||
* @throws Error if prompt is invalid
|
||||
*/
|
||||
protected validatePrompt(prompt: string): void {
|
||||
if (!prompt || typeof prompt !== 'string') {
|
||||
throw new Error('Prompt must be a non-empty string');
|
||||
}
|
||||
|
||||
if (prompt.trim().length === 0) {
|
||||
throw new Error('Prompt cannot be empty or only whitespace');
|
||||
}
|
||||
}
|
||||
}
|
||||
409
packages/tm-core/src/interfaces/configuration.interface.ts
Normal file
409
packages/tm-core/src/interfaces/configuration.interface.ts
Normal file
@@ -0,0 +1,409 @@
|
||||
/**
|
||||
* @fileoverview Configuration interface definitions for the tm-core package
|
||||
* This file defines the contract for configuration management
|
||||
*/
|
||||
|
||||
import type { TaskPriority, TaskComplexity } from '../types/index';
|
||||
|
||||
/**
|
||||
* Model configuration for different AI roles
|
||||
*/
|
||||
export interface ModelConfig {
|
||||
/** Primary model for task generation and updates */
|
||||
main: string;
|
||||
/** Research model for enhanced task analysis (optional) */
|
||||
research?: string;
|
||||
/** Fallback model when primary fails */
|
||||
fallback: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* AI provider configuration
|
||||
*/
|
||||
export interface ProviderConfig {
|
||||
/** Provider name (e.g., 'anthropic', 'openai', 'perplexity') */
|
||||
name: string;
|
||||
/** API key for the provider */
|
||||
apiKey?: string;
|
||||
/** Base URL override */
|
||||
baseUrl?: string;
|
||||
/** Custom configuration options */
|
||||
options?: Record<string, unknown>;
|
||||
/** Whether this provider is enabled */
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Task generation and management settings
|
||||
*/
|
||||
export interface TaskSettings {
|
||||
/** Default priority for new tasks */
|
||||
defaultPriority: TaskPriority;
|
||||
/** Default complexity for analysis */
|
||||
defaultComplexity: TaskComplexity;
|
||||
/** Maximum number of subtasks per task */
|
||||
maxSubtasks: number;
|
||||
/** Maximum number of concurrent tasks */
|
||||
maxConcurrentTasks: number;
|
||||
/** Enable automatic task ID generation */
|
||||
autoGenerateIds: boolean;
|
||||
/** Task ID prefix (e.g., 'TASK-', 'TM-') */
|
||||
taskIdPrefix?: string;
|
||||
/** Enable task dependency validation */
|
||||
validateDependencies: boolean;
|
||||
/** Enable automatic timestamps */
|
||||
enableTimestamps: boolean;
|
||||
/** Enable effort tracking */
|
||||
enableEffortTracking: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tag and context management settings
|
||||
*/
|
||||
export interface TagSettings {
|
||||
/** Enable tag-based task organization */
|
||||
enableTags: boolean;
|
||||
/** Default tag for new tasks */
|
||||
defaultTag: string;
|
||||
/** Maximum number of tags per task */
|
||||
maxTagsPerTask: number;
|
||||
/** Enable automatic tag creation from Git branches */
|
||||
autoCreateFromBranch: boolean;
|
||||
/** Tag naming convention (kebab-case, camelCase, snake_case) */
|
||||
tagNamingConvention: 'kebab-case' | 'camelCase' | 'snake_case';
|
||||
}
|
||||
|
||||
/**
|
||||
* Storage and persistence settings
|
||||
*/
|
||||
export interface StorageSettings {
|
||||
/** Storage backend type */
|
||||
type: 'file' | 'memory' | 'database';
|
||||
/** Base path for file storage */
|
||||
basePath?: string;
|
||||
/** Enable automatic backups */
|
||||
enableBackup: boolean;
|
||||
/** Maximum number of backups to retain */
|
||||
maxBackups: number;
|
||||
/** Enable compression for storage */
|
||||
enableCompression: boolean;
|
||||
/** File encoding for text files */
|
||||
encoding: BufferEncoding;
|
||||
/** Enable atomic file operations */
|
||||
atomicOperations: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retry and resilience settings
|
||||
*/
|
||||
export interface RetrySettings {
|
||||
/** Number of retry attempts for failed operations */
|
||||
retryAttempts: number;
|
||||
/** Base delay between retries in milliseconds */
|
||||
retryDelay: number;
|
||||
/** Maximum delay between retries in milliseconds */
|
||||
maxRetryDelay: number;
|
||||
/** Exponential backoff multiplier */
|
||||
backoffMultiplier: number;
|
||||
/** Request timeout in milliseconds */
|
||||
requestTimeout: number;
|
||||
/** Enable retry for network errors */
|
||||
retryOnNetworkError: boolean;
|
||||
/** Enable retry for rate limit errors */
|
||||
retryOnRateLimit: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logging and debugging settings
|
||||
*/
|
||||
export interface LoggingSettings {
|
||||
/** Enable logging */
|
||||
enabled: boolean;
|
||||
/** Log level (error, warn, info, debug) */
|
||||
level: 'error' | 'warn' | 'info' | 'debug';
|
||||
/** Log file path (optional) */
|
||||
filePath?: string;
|
||||
/** Enable request/response logging */
|
||||
logRequests: boolean;
|
||||
/** Enable performance metrics logging */
|
||||
logPerformance: boolean;
|
||||
/** Enable error stack traces */
|
||||
logStackTraces: boolean;
|
||||
/** Maximum log file size in MB */
|
||||
maxFileSize: number;
|
||||
/** Maximum number of log files to retain */
|
||||
maxFiles: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Security and validation settings
|
||||
*/
|
||||
export interface SecuritySettings {
|
||||
/** Enable API key validation */
|
||||
validateApiKeys: boolean;
|
||||
/** Enable request rate limiting */
|
||||
enableRateLimit: boolean;
|
||||
/** Maximum requests per minute */
|
||||
maxRequestsPerMinute: number;
|
||||
/** Enable input sanitization */
|
||||
sanitizeInputs: boolean;
|
||||
/** Maximum prompt length in characters */
|
||||
maxPromptLength: number;
|
||||
/** Allowed file extensions for imports */
|
||||
allowedFileExtensions: string[];
|
||||
/** Enable CORS protection */
|
||||
enableCors: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main configuration interface for Task Master core
|
||||
*/
|
||||
export interface IConfiguration {
|
||||
/** Project root path */
|
||||
projectPath: string;
|
||||
|
||||
/** Current AI provider name */
|
||||
aiProvider: string;
|
||||
|
||||
/** API keys for different providers */
|
||||
apiKeys: Record<string, string>;
|
||||
|
||||
/** Model configuration for different roles */
|
||||
models: ModelConfig;
|
||||
|
||||
/** Provider configurations */
|
||||
providers: Record<string, ProviderConfig>;
|
||||
|
||||
/** Task management settings */
|
||||
tasks: TaskSettings;
|
||||
|
||||
/** Tag and context settings */
|
||||
tags: TagSettings;
|
||||
|
||||
/** Storage configuration */
|
||||
storage: StorageSettings;
|
||||
|
||||
/** Retry and resilience settings */
|
||||
retry: RetrySettings;
|
||||
|
||||
/** Logging configuration */
|
||||
logging: LoggingSettings;
|
||||
|
||||
/** Security settings */
|
||||
security: SecuritySettings;
|
||||
|
||||
/** Custom user-defined settings */
|
||||
custom?: Record<string, unknown>;
|
||||
|
||||
/** Configuration version for migration purposes */
|
||||
version: string;
|
||||
|
||||
/** Last updated timestamp */
|
||||
lastUpdated: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Partial configuration for updates (all fields optional)
|
||||
*/
|
||||
export type PartialConfiguration = Partial<IConfiguration>;
|
||||
|
||||
/**
|
||||
* Configuration validation result
|
||||
*/
|
||||
export interface ConfigValidationResult {
|
||||
/** Whether the configuration is valid */
|
||||
isValid: boolean;
|
||||
/** Array of error messages */
|
||||
errors: string[];
|
||||
/** Array of warning messages */
|
||||
warnings: string[];
|
||||
/** Suggested fixes */
|
||||
suggestions?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Environment variable configuration mapping
|
||||
*/
|
||||
export interface EnvironmentConfig {
|
||||
/** Mapping of environment variables to config paths */
|
||||
variables: Record<string, string>;
|
||||
/** Prefix for environment variables */
|
||||
prefix: string;
|
||||
/** Whether to override existing config with env vars */
|
||||
override: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration schema definition for validation
|
||||
*/
|
||||
export interface ConfigSchema {
|
||||
/** Schema for the main configuration */
|
||||
properties: Record<string, ConfigProperty>;
|
||||
/** Required properties */
|
||||
required: string[];
|
||||
/** Additional properties allowed */
|
||||
additionalProperties: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration property schema
|
||||
*/
|
||||
export interface ConfigProperty {
|
||||
/** Property type */
|
||||
type: 'string' | 'number' | 'boolean' | 'object' | 'array';
|
||||
/** Property description */
|
||||
description?: string;
|
||||
/** Default value */
|
||||
default?: unknown;
|
||||
/** Allowed values for enums */
|
||||
enum?: unknown[];
|
||||
/** Minimum value (for numbers) */
|
||||
minimum?: number;
|
||||
/** Maximum value (for numbers) */
|
||||
maximum?: number;
|
||||
/** Pattern for string validation */
|
||||
pattern?: string;
|
||||
/** Nested properties (for objects) */
|
||||
properties?: Record<string, ConfigProperty>;
|
||||
/** Array item type (for arrays) */
|
||||
items?: ConfigProperty;
|
||||
/** Whether the property is required */
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default configuration factory
|
||||
*/
|
||||
export interface IConfigurationFactory {
|
||||
/**
|
||||
* Create a default configuration
|
||||
* @param projectPath - Project root path
|
||||
* @returns Default configuration object
|
||||
*/
|
||||
createDefault(projectPath: string): IConfiguration;
|
||||
|
||||
/**
|
||||
* Merge configurations with precedence
|
||||
* @param base - Base configuration
|
||||
* @param override - Override configuration
|
||||
* @returns Merged configuration
|
||||
*/
|
||||
merge(base: IConfiguration, override: PartialConfiguration): IConfiguration;
|
||||
|
||||
/**
|
||||
* Validate configuration against schema
|
||||
* @param config - Configuration to validate
|
||||
* @returns Validation result
|
||||
*/
|
||||
validate(config: IConfiguration): ConfigValidationResult;
|
||||
|
||||
/**
|
||||
* Load configuration from environment variables
|
||||
* @param envConfig - Environment variable mapping
|
||||
* @returns Partial configuration from environment
|
||||
*/
|
||||
loadFromEnvironment(envConfig: EnvironmentConfig): PartialConfiguration;
|
||||
|
||||
/**
|
||||
* Get configuration schema
|
||||
* @returns Configuration schema definition
|
||||
*/
|
||||
getSchema(): ConfigSchema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration manager interface
|
||||
*/
|
||||
export interface IConfigurationManager {
|
||||
/**
|
||||
* Load configuration from file or create default
|
||||
* @param configPath - Path to configuration file
|
||||
* @returns Promise that resolves to configuration
|
||||
*/
|
||||
load(configPath?: string): Promise<IConfiguration>;
|
||||
|
||||
/**
|
||||
* Save configuration to file
|
||||
* @param config - Configuration to save
|
||||
* @param configPath - Optional path override
|
||||
* @returns Promise that resolves when save is complete
|
||||
*/
|
||||
save(config: IConfiguration, configPath?: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Update configuration with partial changes
|
||||
* @param updates - Partial configuration updates
|
||||
* @returns Promise that resolves to updated configuration
|
||||
*/
|
||||
update(updates: PartialConfiguration): Promise<IConfiguration>;
|
||||
|
||||
/**
|
||||
* Get current configuration
|
||||
* @returns Current configuration object
|
||||
*/
|
||||
getConfig(): IConfiguration;
|
||||
|
||||
/**
|
||||
* Watch for configuration changes
|
||||
* @param callback - Function to call when config changes
|
||||
* @returns Function to stop watching
|
||||
*/
|
||||
watch(callback: (config: IConfiguration) => void): () => void;
|
||||
|
||||
/**
|
||||
* Validate current configuration
|
||||
* @returns Validation result
|
||||
*/
|
||||
validate(): ConfigValidationResult;
|
||||
|
||||
/**
|
||||
* Reset configuration to defaults
|
||||
* @returns Promise that resolves when reset is complete
|
||||
*/
|
||||
reset(): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constants for default configuration values
|
||||
*/
|
||||
export const DEFAULT_CONFIG_VALUES = {
|
||||
MODELS: {
|
||||
MAIN: 'claude-3-5-sonnet-20241022',
|
||||
FALLBACK: 'gpt-4o-mini'
|
||||
},
|
||||
TASKS: {
|
||||
DEFAULT_PRIORITY: 'medium' as TaskPriority,
|
||||
DEFAULT_COMPLEXITY: 'moderate' as TaskComplexity,
|
||||
MAX_SUBTASKS: 20,
|
||||
MAX_CONCURRENT: 5,
|
||||
TASK_ID_PREFIX: 'TASK-'
|
||||
},
|
||||
TAGS: {
|
||||
DEFAULT_TAG: 'master',
|
||||
MAX_TAGS_PER_TASK: 10,
|
||||
NAMING_CONVENTION: 'kebab-case' as const
|
||||
},
|
||||
STORAGE: {
|
||||
TYPE: 'file' as const,
|
||||
ENCODING: 'utf8' as BufferEncoding,
|
||||
MAX_BACKUPS: 5
|
||||
},
|
||||
RETRY: {
|
||||
ATTEMPTS: 3,
|
||||
DELAY: 1000,
|
||||
MAX_DELAY: 30000,
|
||||
BACKOFF_MULTIPLIER: 2,
|
||||
TIMEOUT: 30000
|
||||
},
|
||||
LOGGING: {
|
||||
LEVEL: 'info' as const,
|
||||
MAX_FILE_SIZE: 10,
|
||||
MAX_FILES: 5
|
||||
},
|
||||
SECURITY: {
|
||||
MAX_REQUESTS_PER_MINUTE: 60,
|
||||
MAX_PROMPT_LENGTH: 100000,
|
||||
ALLOWED_EXTENSIONS: ['.txt', '.md', '.json']
|
||||
},
|
||||
VERSION: '1.0.0'
|
||||
} as const;
|
||||
16
packages/tm-core/src/interfaces/index.ts
Normal file
16
packages/tm-core/src/interfaces/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* @fileoverview Interface definitions index for the tm-core package
|
||||
* This file exports all interface definitions from their respective modules
|
||||
*/
|
||||
|
||||
// Storage interfaces
|
||||
export type * from './storage.interface';
|
||||
export * from './storage.interface';
|
||||
|
||||
// AI Provider interfaces
|
||||
export type * from './ai-provider.interface';
|
||||
export * from './ai-provider.interface';
|
||||
|
||||
// Configuration interfaces
|
||||
export type * from './configuration.interface';
|
||||
export * from './configuration.interface';
|
||||
238
packages/tm-core/src/interfaces/storage.interface.ts
Normal file
238
packages/tm-core/src/interfaces/storage.interface.ts
Normal file
@@ -0,0 +1,238 @@
|
||||
/**
|
||||
* @fileoverview Storage interface definitions for the tm-core package
|
||||
* This file defines the contract for all storage implementations
|
||||
*/
|
||||
|
||||
import type { Task, TaskMetadata } from '../types/index';
|
||||
|
||||
/**
|
||||
* Interface for storage operations on tasks
|
||||
* All storage implementations must implement this interface
|
||||
*/
|
||||
export interface IStorage {
|
||||
/**
|
||||
* Load all tasks from storage, optionally filtered by tag
|
||||
* @param tag - Optional tag to filter tasks by
|
||||
* @returns Promise that resolves to an array of tasks
|
||||
*/
|
||||
loadTasks(tag?: string): Promise<Task[]>;
|
||||
|
||||
/**
|
||||
* Save tasks to storage, replacing existing tasks
|
||||
* @param tasks - Array of tasks to save
|
||||
* @param tag - Optional tag context for the tasks
|
||||
* @returns Promise that resolves when save is complete
|
||||
*/
|
||||
saveTasks(tasks: Task[], tag?: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Append new tasks to existing storage without replacing
|
||||
* @param tasks - Array of tasks to append
|
||||
* @param tag - Optional tag context for the tasks
|
||||
* @returns Promise that resolves when append is complete
|
||||
*/
|
||||
appendTasks(tasks: Task[], tag?: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Update a specific task by ID
|
||||
* @param taskId - ID of the task to update
|
||||
* @param updates - Partial task object with fields to update
|
||||
* @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>;
|
||||
|
||||
/**
|
||||
* Delete a task by ID
|
||||
* @param taskId - ID of the task to delete
|
||||
* @param tag - Optional tag context for the task
|
||||
* @returns Promise that resolves when deletion is complete
|
||||
*/
|
||||
deleteTask(taskId: string, tag?: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Check if tasks exist in storage for the given tag
|
||||
* @param tag - Optional tag to check existence for
|
||||
* @returns Promise that resolves to boolean indicating existence
|
||||
*/
|
||||
exists(tag?: string): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Load metadata about the task collection
|
||||
* @param tag - Optional tag to get metadata for
|
||||
* @returns Promise that resolves to task metadata
|
||||
*/
|
||||
loadMetadata(tag?: string): Promise<TaskMetadata | null>;
|
||||
|
||||
/**
|
||||
* Save metadata about the task collection
|
||||
* @param metadata - Metadata object to save
|
||||
* @param tag - Optional tag context for the metadata
|
||||
* @returns Promise that resolves when save is complete
|
||||
*/
|
||||
saveMetadata(metadata: TaskMetadata, tag?: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Get all available tags in storage
|
||||
* @returns Promise that resolves to array of available tags
|
||||
*/
|
||||
getAllTags(): Promise<string[]>;
|
||||
|
||||
/**
|
||||
* Delete all tasks and metadata for a specific tag
|
||||
* @param tag - Tag to delete
|
||||
* @returns Promise that resolves when deletion is complete
|
||||
*/
|
||||
deleteTag(tag: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Rename a tag (move all tasks from old tag to new tag)
|
||||
* @param oldTag - Current tag name
|
||||
* @param newTag - New tag name
|
||||
* @returns Promise that resolves when rename is complete
|
||||
*/
|
||||
renameTag(oldTag: string, newTag: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Copy all tasks from one tag to another
|
||||
* @param sourceTag - Source tag to copy from
|
||||
* @param targetTag - Target tag to copy to
|
||||
* @returns Promise that resolves when copy is complete
|
||||
*/
|
||||
copyTag(sourceTag: string, targetTag: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Initialize storage (create necessary directories, files, etc.)
|
||||
* @returns Promise that resolves when initialization is complete
|
||||
*/
|
||||
initialize(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Clean up and close storage connections
|
||||
* @returns Promise that resolves when cleanup is complete
|
||||
*/
|
||||
close(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Get storage statistics (file sizes, task counts, etc.)
|
||||
* @returns Promise that resolves to storage statistics
|
||||
*/
|
||||
getStats(): Promise<StorageStats>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Storage statistics interface
|
||||
*/
|
||||
export interface StorageStats {
|
||||
/** Total number of tasks across all tags */
|
||||
totalTasks: number;
|
||||
/** Total number of tags */
|
||||
totalTags: number;
|
||||
/** Storage size in bytes */
|
||||
storageSize: number;
|
||||
/** Last modified timestamp */
|
||||
lastModified: string;
|
||||
/** Available tags with task counts */
|
||||
tagStats: Array<{
|
||||
tag: string;
|
||||
taskCount: number;
|
||||
lastModified: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for storage implementations
|
||||
*/
|
||||
export interface StorageConfig {
|
||||
/** Base path for storage */
|
||||
basePath: string;
|
||||
/** Enable backup creation */
|
||||
enableBackup?: boolean;
|
||||
/** Maximum number of backups to keep */
|
||||
maxBackups?: number;
|
||||
/** Enable compression for storage */
|
||||
enableCompression?: boolean;
|
||||
/** File encoding (default: utf8) */
|
||||
encoding?: BufferEncoding;
|
||||
/** Enable atomic writes */
|
||||
atomicWrites?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base abstract class for storage implementations
|
||||
* Provides common functionality and enforces the interface
|
||||
*/
|
||||
export abstract class BaseStorage implements IStorage {
|
||||
protected config: StorageConfig;
|
||||
|
||||
constructor(config: StorageConfig) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
// Abstract methods that must be implemented by concrete classes
|
||||
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 deleteTask(taskId: string, tag?: string): Promise<void>;
|
||||
abstract exists(tag?: string): Promise<boolean>;
|
||||
abstract loadMetadata(tag?: string): Promise<TaskMetadata | null>;
|
||||
abstract saveMetadata(metadata: TaskMetadata, tag?: string): Promise<void>;
|
||||
abstract getAllTags(): Promise<string[]>;
|
||||
abstract deleteTag(tag: string): Promise<void>;
|
||||
abstract renameTag(oldTag: string, newTag: string): Promise<void>;
|
||||
abstract copyTag(sourceTag: string, targetTag: string): Promise<void>;
|
||||
abstract initialize(): Promise<void>;
|
||||
abstract close(): Promise<void>;
|
||||
abstract getStats(): Promise<StorageStats>;
|
||||
|
||||
/**
|
||||
* Utility method to generate backup filename
|
||||
* @param originalPath - Original file path
|
||||
* @returns Backup file path with timestamp
|
||||
*/
|
||||
protected generateBackupPath(originalPath: string): string {
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const parts = originalPath.split('.');
|
||||
const extension = parts.pop();
|
||||
const baseName = parts.join('.');
|
||||
return `${baseName}.backup.${timestamp}.${extension}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to validate task data before storage operations
|
||||
* @param task - Task to validate
|
||||
* @throws Error if task is invalid
|
||||
*/
|
||||
protected validateTask(task: Task): void {
|
||||
if (!task.id) {
|
||||
throw new Error('Task ID is required');
|
||||
}
|
||||
if (!task.title) {
|
||||
throw new Error('Task title is required');
|
||||
}
|
||||
if (!task.description) {
|
||||
throw new Error('Task description is required');
|
||||
}
|
||||
if (!task.status) {
|
||||
throw new Error('Task status is required');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to sanitize tag names for file system safety
|
||||
* @param tag - Tag name to sanitize
|
||||
* @returns Sanitized tag name
|
||||
*/
|
||||
protected sanitizeTag(tag: string): string {
|
||||
return tag.replace(/[^a-zA-Z0-9-_]/g, '-').toLowerCase();
|
||||
}
|
||||
}
|
||||
39
packages/tm-core/src/parser/index.ts
Normal file
39
packages/tm-core/src/parser/index.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* @fileoverview Task parsing functionality for the tm-core package
|
||||
* This file exports all parsing-related classes and functions
|
||||
*/
|
||||
|
||||
import type { PlaceholderTask } from '../types/index';
|
||||
|
||||
// Parser implementations will be defined here
|
||||
// export * from './prd-parser.js';
|
||||
// export * from './task-parser.js';
|
||||
// export * from './markdown-parser.js';
|
||||
|
||||
// Placeholder exports - these will be implemented in later tasks
|
||||
export interface TaskParser {
|
||||
parse(content: string): Promise<PlaceholderTask[]>;
|
||||
validate(content: string): Promise<boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated This is a placeholder class that will be properly implemented in later tasks
|
||||
*/
|
||||
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('-'));
|
||||
return lines.map((line, index) => ({
|
||||
id: `task-${index + 1}`,
|
||||
title: line.trim().replace(/^-\s*/, ''),
|
||||
status: 'pending' as const,
|
||||
priority: 'medium' as const
|
||||
}));
|
||||
}
|
||||
|
||||
async validate(content: string): Promise<boolean> {
|
||||
return content.trim().length > 0;
|
||||
}
|
||||
}
|
||||
76
packages/tm-core/src/providers/base-provider.ts
Normal file
76
packages/tm-core/src/providers/base-provider.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* @fileoverview Base provider implementation for AI providers in tm-core
|
||||
* Provides common functionality and properties for all AI provider implementations
|
||||
*/
|
||||
|
||||
import {
|
||||
IAIProvider,
|
||||
AIOptions,
|
||||
AIResponse,
|
||||
AIModel,
|
||||
ProviderInfo,
|
||||
ProviderUsageStats
|
||||
} from '../interfaces/ai-provider.interface.js';
|
||||
|
||||
/**
|
||||
* Configuration interface for BaseProvider
|
||||
*/
|
||||
export interface BaseProviderConfig {
|
||||
/** API key for the provider */
|
||||
apiKey: string;
|
||||
/** Optional model ID to use */
|
||||
model?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract base class providing common functionality for all AI providers
|
||||
* Implements the IAIProvider interface with shared properties and basic methods
|
||||
*/
|
||||
export abstract class BaseProvider implements IAIProvider {
|
||||
/** API key for authentication */
|
||||
protected apiKey: string;
|
||||
/** Current model being used */
|
||||
protected model: string;
|
||||
/** Maximum number of retry attempts */
|
||||
protected maxRetries: number = 3;
|
||||
/** Delay between retries in milliseconds */
|
||||
protected retryDelay: number = 1000;
|
||||
|
||||
/**
|
||||
* Constructor for BaseProvider
|
||||
* @param config - Configuration object with apiKey and optional model
|
||||
*/
|
||||
constructor(config: BaseProviderConfig) {
|
||||
this.apiKey = config.apiKey;
|
||||
this.model = config.model || this.getDefaultModel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the currently configured model
|
||||
* @returns Current model ID
|
||||
*/
|
||||
getModel(): string {
|
||||
return this.model;
|
||||
}
|
||||
|
||||
// Abstract methods that concrete providers must implement
|
||||
abstract generateCompletion(
|
||||
prompt: string,
|
||||
options?: AIOptions
|
||||
): Promise<AIResponse>;
|
||||
abstract generateStreamingCompletion(
|
||||
prompt: string,
|
||||
options?: AIOptions
|
||||
): AsyncIterator<Partial<AIResponse>>;
|
||||
abstract calculateTokens(text: string, model?: string): number;
|
||||
abstract getName(): string;
|
||||
abstract setModel(model: string): void;
|
||||
abstract getDefaultModel(): string;
|
||||
abstract isAvailable(): Promise<boolean>;
|
||||
abstract getProviderInfo(): ProviderInfo;
|
||||
abstract getAvailableModels(): AIModel[];
|
||||
abstract validateCredentials(): Promise<boolean>;
|
||||
abstract getUsageStats(): Promise<ProviderUsageStats | null>;
|
||||
abstract initialize(): Promise<void>;
|
||||
abstract close(): Promise<void>;
|
||||
}
|
||||
27
packages/tm-core/src/providers/index.ts
Normal file
27
packages/tm-core/src/providers/index.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* @fileoverview AI provider implementations for the tm-core package
|
||||
* This file exports all AI provider classes and interfaces
|
||||
*/
|
||||
|
||||
// Provider interfaces and implementations
|
||||
export * from './base-provider.js';
|
||||
// export * from './anthropic-provider.js';
|
||||
// export * from './openai-provider.js';
|
||||
// export * from './perplexity-provider.js';
|
||||
|
||||
// Placeholder exports - these will be implemented in later tasks
|
||||
export interface AIProvider {
|
||||
name: string;
|
||||
generateResponse(prompt: string): Promise<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated This is a placeholder class that will be properly implemented in later tasks
|
||||
*/
|
||||
export class PlaceholderProvider implements AIProvider {
|
||||
name = 'placeholder';
|
||||
|
||||
async generateResponse(prompt: string): Promise<string> {
|
||||
return `Placeholder response for: ${prompt}`;
|
||||
}
|
||||
}
|
||||
356
packages/tm-core/src/storage/file-storage.ts
Normal file
356
packages/tm-core/src/storage/file-storage.ts
Normal file
@@ -0,0 +1,356 @@
|
||||
/**
|
||||
* File-based storage implementation for Task Master
|
||||
*/
|
||||
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import type { Task, TaskMetadata } from '../types/index.js';
|
||||
import { BaseStorage, type StorageStats } from './storage.interface.js';
|
||||
|
||||
/**
|
||||
* File storage data structure
|
||||
*/
|
||||
interface FileStorageData {
|
||||
tasks: Task[];
|
||||
metadata: TaskMetadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* File-based storage implementation using JSON files
|
||||
*/
|
||||
export class FileStorage extends BaseStorage {
|
||||
private readonly projectPath: string;
|
||||
private readonly basePath: string;
|
||||
private readonly tasksDir: string;
|
||||
private fileLocks: Map<string, Promise<void>> = new Map();
|
||||
|
||||
constructor(projectPath: string, config = {}) {
|
||||
super(config);
|
||||
this.projectPath = projectPath;
|
||||
this.basePath = path.join(projectPath, '.taskmaster');
|
||||
this.tasksDir = path.join(this.basePath, 'tasks');
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize storage by creating necessary directories
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
await this.ensureDirectoryExists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Close storage and cleanup resources
|
||||
*/
|
||||
async close(): Promise<void> {
|
||||
// Wait for any pending file operations
|
||||
const locks = Array.from(this.fileLocks.values());
|
||||
if (locks.length > 0) {
|
||||
await Promise.all(locks);
|
||||
}
|
||||
this.fileLocks.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get statistics about the storage
|
||||
*/
|
||||
async getStats(): Promise<StorageStats> {
|
||||
const tags = await this.getAllTags();
|
||||
let totalTasks = 0;
|
||||
let lastModified = '';
|
||||
|
||||
for (const tag of tags) {
|
||||
const filePath = this.getTasksPath(tag === 'default' ? undefined : tag);
|
||||
try {
|
||||
const stats = await fs.stat(filePath);
|
||||
const data = await this.readJsonFile(filePath);
|
||||
if (data?.tasks) {
|
||||
totalTasks += data.tasks.length;
|
||||
}
|
||||
if (stats.mtime.toISOString() > lastModified) {
|
||||
lastModified = stats.mtime.toISOString();
|
||||
}
|
||||
} catch {
|
||||
// Ignore missing files
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
totalTasks,
|
||||
totalTags: tags.length,
|
||||
lastModified: lastModified || new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Load tasks from file
|
||||
*/
|
||||
async loadTasks(tag?: string): Promise<Task[]> {
|
||||
const filePath = this.getTasksPath(tag);
|
||||
|
||||
try {
|
||||
const data = await this.readJsonFile(filePath);
|
||||
return data?.tasks || [];
|
||||
} catch (error: any) {
|
||||
if (error.code === 'ENOENT') {
|
||||
return []; // File doesn't exist, return empty array
|
||||
}
|
||||
throw new Error(`Failed to load tasks: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save tasks to file
|
||||
*/
|
||||
async saveTasks(tasks: Task[], tag?: string): Promise<void> {
|
||||
const filePath = this.getTasksPath(tag);
|
||||
|
||||
// Ensure directory exists
|
||||
await this.ensureDirectoryExists();
|
||||
|
||||
// Create data structure with metadata
|
||||
const data: FileStorageData = {
|
||||
tasks,
|
||||
metadata: {
|
||||
version: '1.0.0',
|
||||
lastModified: new Date().toISOString(),
|
||||
taskCount: tasks.length,
|
||||
completedCount: tasks.filter((t) => t.status === 'done').length,
|
||||
tags: tag ? [tag] : []
|
||||
}
|
||||
};
|
||||
|
||||
// Write with file locking
|
||||
await this.writeJsonFile(filePath, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if tasks file exists
|
||||
*/
|
||||
async exists(tag?: string): Promise<boolean> {
|
||||
const filePath = this.getTasksPath(tag);
|
||||
|
||||
try {
|
||||
await fs.access(filePath, fs.constants.F_OK);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available tags
|
||||
*/
|
||||
async getAllTags(): Promise<string[]> {
|
||||
try {
|
||||
await this.ensureDirectoryExists();
|
||||
const files = await fs.readdir(this.tasksDir);
|
||||
|
||||
const tags: string[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
if (file.endsWith('.json')) {
|
||||
if (file === 'tasks.json') {
|
||||
tags.push('default');
|
||||
} else if (!file.includes('.backup.')) {
|
||||
// Extract tag name from filename (remove .json extension)
|
||||
tags.push(file.slice(0, -5));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tags;
|
||||
} catch (error: any) {
|
||||
if (error.code === 'ENOENT') {
|
||||
return [];
|
||||
}
|
||||
throw new Error(`Failed to get tags: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load metadata from file
|
||||
*/
|
||||
async loadMetadata(tag?: string): Promise<TaskMetadata | null> {
|
||||
const filePath = this.getTasksPath(tag);
|
||||
|
||||
try {
|
||||
const data = await this.readJsonFile(filePath);
|
||||
return data?.metadata || null;
|
||||
} catch (error: any) {
|
||||
if (error.code === 'ENOENT') {
|
||||
return null;
|
||||
}
|
||||
throw new Error(`Failed to load metadata: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save metadata (stored with tasks)
|
||||
*/
|
||||
async saveMetadata(metadata: TaskMetadata, tag?: string): Promise<void> {
|
||||
const tasks = await this.loadTasks(tag);
|
||||
const filePath = this.getTasksPath(tag);
|
||||
|
||||
const data: FileStorageData = {
|
||||
tasks,
|
||||
metadata
|
||||
};
|
||||
|
||||
await this.writeJsonFile(filePath, data);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Private Helper Methods
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get the file path for tasks based on tag
|
||||
*/
|
||||
private getTasksPath(tag?: string): string {
|
||||
if (tag) {
|
||||
const sanitizedTag = this.sanitizeTag(tag);
|
||||
return path.join(this.tasksDir, `${sanitizedTag}.json`);
|
||||
}
|
||||
return path.join(this.tasksDir, 'tasks.json');
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the storage directory structure exists
|
||||
*/
|
||||
private async ensureDirectoryExists(): Promise<void> {
|
||||
try {
|
||||
await fs.mkdir(this.tasksDir, { recursive: true });
|
||||
} catch (error: any) {
|
||||
throw new Error(`Failed to create storage directory: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and parse JSON file with error handling
|
||||
*/
|
||||
private async readJsonFile(
|
||||
filePath: string
|
||||
): Promise<FileStorageData | null> {
|
||||
try {
|
||||
const content = await fs.readFile(filePath, 'utf-8');
|
||||
return JSON.parse(content);
|
||||
} catch (error: any) {
|
||||
if (error.code === 'ENOENT') {
|
||||
throw error; // Re-throw ENOENT for caller to handle
|
||||
}
|
||||
if (error instanceof SyntaxError) {
|
||||
throw new Error(`Invalid JSON in file ${filePath}: ${error.message}`);
|
||||
}
|
||||
throw new Error(`Failed to read file ${filePath}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write JSON file with atomic operation using temp file
|
||||
*/
|
||||
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);
|
||||
|
||||
if (existingLock) {
|
||||
await existingLock;
|
||||
}
|
||||
|
||||
const lockPromise = this.performWrite(filePath, data);
|
||||
this.fileLocks.set(lockKey, lockPromise);
|
||||
|
||||
try {
|
||||
await lockPromise;
|
||||
} finally {
|
||||
this.fileLocks.delete(lockKey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the actual write operation
|
||||
*/
|
||||
private async performWrite(
|
||||
filePath: string,
|
||||
data: FileStorageData
|
||||
): Promise<void> {
|
||||
const tempPath = `${filePath}.tmp`;
|
||||
|
||||
try {
|
||||
// Write to temp file first
|
||||
const content = JSON.stringify(data, null, 2);
|
||||
await fs.writeFile(tempPath, content, 'utf-8');
|
||||
|
||||
// Create backup if configured
|
||||
if (this.config.autoBackup && (await this.exists())) {
|
||||
await this.createBackup(filePath);
|
||||
}
|
||||
|
||||
// Atomic rename
|
||||
await fs.rename(tempPath, filePath);
|
||||
} catch (error: any) {
|
||||
// Clean up temp file if it exists
|
||||
try {
|
||||
await fs.unlink(tempPath);
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
|
||||
throw new Error(`Failed to write file ${filePath}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a backup of the file
|
||||
*/
|
||||
private async createBackup(filePath: string): Promise<void> {
|
||||
try {
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const backupPath = this.getBackupPath(filePath, timestamp);
|
||||
await fs.copyFile(filePath, backupPath);
|
||||
|
||||
// Clean up old backups if needed
|
||||
if (this.config.maxBackups) {
|
||||
await this.cleanupOldBackups(filePath);
|
||||
}
|
||||
} catch {
|
||||
// Backup failures are non-critical
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove old backup files beyond the max limit
|
||||
*/
|
||||
private async cleanupOldBackups(originalPath: string): Promise<void> {
|
||||
const dir = path.dirname(originalPath);
|
||||
const basename = path.basename(originalPath, '.json');
|
||||
|
||||
try {
|
||||
const files = await fs.readdir(dir);
|
||||
const backupFiles = files
|
||||
.filter(
|
||||
(f) => f.startsWith(`${basename}.backup.`) && f.endsWith('.json')
|
||||
)
|
||||
.sort()
|
||||
.reverse();
|
||||
|
||||
// Remove backups beyond the limit
|
||||
const toRemove = backupFiles.slice(this.config.maxBackups!);
|
||||
for (const file of toRemove) {
|
||||
try {
|
||||
await fs.unlink(path.join(dir, file));
|
||||
} catch {
|
||||
// Ignore individual file deletion errors
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Cleanup failures are non-critical
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export as default for convenience
|
||||
export default FileStorage;
|
||||
40
packages/tm-core/src/storage/index.ts
Normal file
40
packages/tm-core/src/storage/index.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* @fileoverview Storage layer for the tm-core package
|
||||
* This file exports all storage-related classes and interfaces
|
||||
*/
|
||||
|
||||
// Storage implementations will be defined here
|
||||
// export * from './file-storage.js';
|
||||
// export * from './memory-storage.js';
|
||||
// export * from './storage-interface.js';
|
||||
|
||||
// Placeholder exports - these will be implemented in later tasks
|
||||
export interface StorageAdapter {
|
||||
read(path: string): Promise<string | null>;
|
||||
write(path: string, data: string): Promise<void>;
|
||||
exists(path: string): Promise<boolean>;
|
||||
delete(path: string): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated This is a placeholder class that will be properly implemented in later tasks
|
||||
*/
|
||||
export class PlaceholderStorage implements StorageAdapter {
|
||||
private data = new Map<string, string>();
|
||||
|
||||
async read(path: string): Promise<string | null> {
|
||||
return this.data.get(path) || null;
|
||||
}
|
||||
|
||||
async write(path: string, data: string): Promise<void> {
|
||||
this.data.set(path, data);
|
||||
}
|
||||
|
||||
async exists(path: string): Promise<boolean> {
|
||||
return this.data.has(path);
|
||||
}
|
||||
|
||||
async delete(path: string): Promise<void> {
|
||||
this.data.delete(path);
|
||||
}
|
||||
}
|
||||
289
packages/tm-core/src/storage/storage.interface.ts
Normal file
289
packages/tm-core/src/storage/storage.interface.ts
Normal file
@@ -0,0 +1,289 @@
|
||||
/**
|
||||
* Storage interface and base implementation for Task Master
|
||||
*/
|
||||
|
||||
import type {
|
||||
Task,
|
||||
TaskMetadata,
|
||||
TaskFilter,
|
||||
TaskSortOptions
|
||||
} from '../types/index.js';
|
||||
|
||||
/**
|
||||
* Storage statistics
|
||||
*/
|
||||
export interface StorageStats {
|
||||
totalTasks: number;
|
||||
totalTags: number;
|
||||
lastModified: string;
|
||||
storageSize?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Storage configuration options
|
||||
*/
|
||||
export interface StorageConfig {
|
||||
basePath?: string;
|
||||
autoBackup?: boolean;
|
||||
backupInterval?: number;
|
||||
maxBackups?: number;
|
||||
compression?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Core storage interface for task persistence
|
||||
*/
|
||||
export interface IStorage {
|
||||
// Core task operations
|
||||
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>;
|
||||
deleteTask(taskId: string, tag?: string): Promise<boolean>;
|
||||
exists(tag?: string): Promise<boolean>;
|
||||
|
||||
// Metadata operations
|
||||
loadMetadata(tag?: string): Promise<TaskMetadata | null>;
|
||||
saveMetadata(metadata: TaskMetadata, tag?: string): Promise<void>;
|
||||
|
||||
// Tag management
|
||||
getAllTags(): Promise<string[]>;
|
||||
deleteTag(tag: string): Promise<boolean>;
|
||||
renameTag(oldTag: string, newTag: string): Promise<boolean>;
|
||||
copyTag(sourceTag: string, targetTag: string): Promise<boolean>;
|
||||
|
||||
// Advanced operations
|
||||
searchTasks(filter: TaskFilter, tag?: string): Promise<Task[]>;
|
||||
sortTasks(tasks: Task[], options: TaskSortOptions): Task[];
|
||||
|
||||
// Lifecycle methods
|
||||
initialize(): Promise<void>;
|
||||
close(): Promise<void>;
|
||||
getStats(): Promise<StorageStats>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract base class for storage implementations
|
||||
*/
|
||||
export abstract class BaseStorage implements IStorage {
|
||||
protected config: StorageConfig;
|
||||
|
||||
constructor(config: StorageConfig = {}) {
|
||||
this.config = {
|
||||
autoBackup: false,
|
||||
backupInterval: 3600000, // 1 hour
|
||||
maxBackups: 10,
|
||||
compression: false,
|
||||
...config
|
||||
};
|
||||
}
|
||||
|
||||
// Abstract methods that must be implemented by subclasses
|
||||
abstract loadTasks(tag?: string): Promise<Task[]>;
|
||||
abstract saveTasks(tasks: Task[], tag?: string): Promise<void>;
|
||||
abstract exists(tag?: string): Promise<boolean>;
|
||||
abstract initialize(): Promise<void>;
|
||||
abstract close(): Promise<void>;
|
||||
abstract getAllTags(): Promise<string[]>;
|
||||
abstract getStats(): Promise<StorageStats>;
|
||||
|
||||
// Default implementations that can be overridden
|
||||
async appendTasks(tasks: Task[], tag?: string): Promise<void> {
|
||||
const existingTasks = await this.loadTasks(tag);
|
||||
const existingIds = new Set(existingTasks.map((t) => t.id));
|
||||
const newTasks = tasks.filter((t) => !existingIds.has(t.id));
|
||||
const mergedTasks = [...existingTasks, ...newTasks];
|
||||
await this.saveTasks(mergedTasks, tag);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if (taskIndex === -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
tasks[taskIndex] = {
|
||||
...tasks[taskIndex],
|
||||
...updates,
|
||||
id: taskId, // Ensure ID cannot be changed
|
||||
updatedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
await this.saveTasks(tasks, tag);
|
||||
return true;
|
||||
}
|
||||
|
||||
async deleteTask(taskId: string, tag?: string): Promise<boolean> {
|
||||
const tasks = await this.loadTasks(tag);
|
||||
const filteredTasks = tasks.filter((t) => t.id !== taskId);
|
||||
|
||||
if (tasks.length === filteredTasks.length) {
|
||||
return false; // Task not found
|
||||
}
|
||||
|
||||
await this.saveTasks(filteredTasks, tag);
|
||||
return true;
|
||||
}
|
||||
|
||||
async loadMetadata(tag?: string): Promise<TaskMetadata | null> {
|
||||
const tasks = await this.loadTasks(tag);
|
||||
if (tasks.length === 0) return null;
|
||||
|
||||
const completedCount = tasks.filter((t) => t.status === 'done').length;
|
||||
|
||||
return {
|
||||
version: '1.0.0',
|
||||
lastModified: new Date().toISOString(),
|
||||
taskCount: tasks.length,
|
||||
completedCount
|
||||
};
|
||||
}
|
||||
|
||||
async saveMetadata(metadata: TaskMetadata, tag?: string): Promise<void> {
|
||||
// Default implementation: metadata is derived from tasks
|
||||
// Subclasses can override if they store metadata separately
|
||||
}
|
||||
|
||||
async deleteTag(tag: string): Promise<boolean> {
|
||||
if (await this.exists(tag)) {
|
||||
await this.saveTasks([], tag);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async renameTag(oldTag: string, newTag: string): Promise<boolean> {
|
||||
if (!(await this.exists(oldTag))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const tasks = await this.loadTasks(oldTag);
|
||||
await this.saveTasks(tasks, newTag);
|
||||
await this.deleteTag(oldTag);
|
||||
return true;
|
||||
}
|
||||
|
||||
async copyTag(sourceTag: string, targetTag: string): Promise<boolean> {
|
||||
if (!(await this.exists(sourceTag))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const tasks = await this.loadTasks(sourceTag);
|
||||
await this.saveTasks(tasks, targetTag);
|
||||
return true;
|
||||
}
|
||||
|
||||
async searchTasks(filter: TaskFilter, tag?: string): Promise<Task[]> {
|
||||
const tasks = await this.loadTasks(tag);
|
||||
|
||||
return tasks.filter((task) => {
|
||||
// Status filter
|
||||
if (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];
|
||||
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))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Subtasks filter
|
||||
if (filter.hasSubtasks !== undefined) {
|
||||
const hasSubtasks = task.subtasks && task.subtasks.length > 0;
|
||||
if (hasSubtasks !== filter.hasSubtasks) return false;
|
||||
}
|
||||
|
||||
// Search filter
|
||||
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;
|
||||
}
|
||||
|
||||
// Assignee filter
|
||||
if (filter.assignee && task.assignee !== filter.assignee) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Complexity filter
|
||||
if (filter.complexity) {
|
||||
const complexities = Array.isArray(filter.complexity)
|
||||
? filter.complexity
|
||||
: [filter.complexity];
|
||||
if (!task.complexity || !complexities.includes(task.complexity))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
sortTasks(tasks: Task[], options: TaskSortOptions): Task[] {
|
||||
return [...tasks].sort((a, b) => {
|
||||
const aValue = a[options.field];
|
||||
const bValue = b[options.field];
|
||||
|
||||
if (aValue === undefined || bValue === undefined) return 0;
|
||||
|
||||
let comparison = 0;
|
||||
if (aValue < bValue) comparison = -1;
|
||||
if (aValue > bValue) comparison = 1;
|
||||
|
||||
return options.direction === 'asc' ? comparison : -comparison;
|
||||
});
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
protected validateTask(task: Task): void {
|
||||
if (!task.id || typeof task.id !== 'string') {
|
||||
throw new Error('Task must have a valid string ID');
|
||||
}
|
||||
if (!task.title || typeof task.title !== 'string') {
|
||||
throw new Error('Task must have a valid title');
|
||||
}
|
||||
if (!task.status) {
|
||||
throw new Error('Task must have a valid status');
|
||||
}
|
||||
}
|
||||
|
||||
protected sanitizeTag(tag: string): string {
|
||||
// Remove or replace characters that might cause filesystem issues
|
||||
return tag.replace(/[^a-zA-Z0-9-_]/g, '_').toLowerCase();
|
||||
}
|
||||
|
||||
protected getBackupPath(originalPath: string, timestamp: string): string {
|
||||
const parts = originalPath.split('.');
|
||||
const ext = parts.pop();
|
||||
return `${parts.join('.')}.backup.${timestamp}.${ext}`;
|
||||
}
|
||||
}
|
||||
228
packages/tm-core/src/types/index.ts
Normal file
228
packages/tm-core/src/types/index.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
/**
|
||||
* Core type definitions for Task Master
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// Type Literals
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Task status values
|
||||
*/
|
||||
export type TaskStatus =
|
||||
| 'pending'
|
||||
| 'in-progress'
|
||||
| 'done'
|
||||
| 'deferred'
|
||||
| 'cancelled'
|
||||
| 'blocked'
|
||||
| 'review';
|
||||
|
||||
/**
|
||||
* Task priority levels
|
||||
*/
|
||||
export type TaskPriority = 'low' | 'medium' | 'high' | 'critical';
|
||||
|
||||
/**
|
||||
* Task complexity levels
|
||||
*/
|
||||
export type TaskComplexity = 'simple' | 'moderate' | 'complex' | 'very-complex';
|
||||
|
||||
// ============================================================================
|
||||
// Core Interfaces
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Base task interface
|
||||
*/
|
||||
export interface Task {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
status: TaskStatus;
|
||||
priority: TaskPriority;
|
||||
dependencies: string[];
|
||||
details: string;
|
||||
testStrategy: string;
|
||||
subtasks: Subtask[];
|
||||
|
||||
// Optional enhanced properties
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
effort?: number;
|
||||
actualEffort?: number;
|
||||
tags?: string[];
|
||||
assignee?: string;
|
||||
complexity?: TaskComplexity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subtask interface extending Task with numeric ID
|
||||
*/
|
||||
export interface Subtask extends Omit<Task, 'id' | 'subtasks'> {
|
||||
id: number;
|
||||
parentId: string;
|
||||
subtasks?: never; // Subtasks cannot have their own subtasks
|
||||
}
|
||||
|
||||
/**
|
||||
* Task metadata for tracking overall project state
|
||||
*/
|
||||
export interface TaskMetadata {
|
||||
version: string;
|
||||
lastModified: string;
|
||||
taskCount: number;
|
||||
completedCount: number;
|
||||
projectName?: string;
|
||||
description?: string;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Task collection with metadata
|
||||
*/
|
||||
export interface TaskCollection {
|
||||
tasks: Task[];
|
||||
metadata: TaskMetadata;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Utility Types
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Type for creating a new task (without generated fields)
|
||||
*/
|
||||
export type CreateTask = Omit<
|
||||
Task,
|
||||
'id' | 'createdAt' | 'updatedAt' | 'subtasks'
|
||||
> & {
|
||||
subtasks?: Omit<Subtask, 'id' | 'parentId' | 'createdAt' | 'updatedAt'>[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Type for updating a task (all fields optional except ID)
|
||||
*/
|
||||
export type UpdateTask = Partial<Omit<Task, 'id'>> & {
|
||||
id: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Type for task filters
|
||||
*/
|
||||
export interface TaskFilter {
|
||||
status?: TaskStatus | TaskStatus[];
|
||||
priority?: TaskPriority | TaskPriority[];
|
||||
tags?: string[];
|
||||
hasSubtasks?: boolean;
|
||||
search?: string;
|
||||
assignee?: string;
|
||||
complexity?: TaskComplexity | TaskComplexity[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Type for sort options
|
||||
*/
|
||||
export interface TaskSortOptions {
|
||||
field: keyof Task;
|
||||
direction: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Type Guards
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Type guard to check if a value is a valid TaskStatus
|
||||
*/
|
||||
export function isTaskStatus(value: unknown): value is TaskStatus {
|
||||
return (
|
||||
typeof value === 'string' &&
|
||||
[
|
||||
'pending',
|
||||
'in-progress',
|
||||
'done',
|
||||
'deferred',
|
||||
'cancelled',
|
||||
'blocked',
|
||||
'review'
|
||||
].includes(value)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if a value is a valid TaskComplexity
|
||||
*/
|
||||
export function isTaskComplexity(value: unknown): value is TaskComplexity {
|
||||
return (
|
||||
typeof value === 'string' &&
|
||||
['simple', 'moderate', 'complex', 'very-complex'].includes(value)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an object is a Task
|
||||
*/
|
||||
export function isTask(obj: unknown): obj is Task {
|
||||
if (!obj || typeof obj !== 'object') return false;
|
||||
const task = obj as Record<string, unknown>;
|
||||
|
||||
return (
|
||||
typeof task.id === 'string' &&
|
||||
typeof task.title === 'string' &&
|
||||
typeof task.description === 'string' &&
|
||||
isTaskStatus(task.status) &&
|
||||
isTaskPriority(task.priority) &&
|
||||
Array.isArray(task.dependencies) &&
|
||||
typeof task.details === 'string' &&
|
||||
typeof task.testStrategy === 'string' &&
|
||||
Array.isArray(task.subtasks)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an object is a Subtask
|
||||
*/
|
||||
export function isSubtask(obj: unknown): obj is Subtask {
|
||||
if (!obj || typeof obj !== 'object') return false;
|
||||
const subtask = obj as Record<string, unknown>;
|
||||
|
||||
return (
|
||||
typeof subtask.id === 'number' &&
|
||||
typeof subtask.parentId === 'string' &&
|
||||
typeof subtask.title === 'string' &&
|
||||
typeof subtask.description === 'string' &&
|
||||
isTaskStatus(subtask.status) &&
|
||||
isTaskPriority(subtask.priority) &&
|
||||
!('subtasks' in subtask)
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Deprecated Types (for backwards compatibility)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @deprecated Use TaskStatus instead
|
||||
*/
|
||||
export type Status = TaskStatus;
|
||||
|
||||
/**
|
||||
* @deprecated Use TaskPriority instead
|
||||
*/
|
||||
export type Priority = TaskPriority;
|
||||
|
||||
/**
|
||||
* @deprecated Use TaskComplexity instead
|
||||
*/
|
||||
export type Complexity = TaskComplexity;
|
||||
9
packages/tm-core/src/types/legacy.ts
Normal file
9
packages/tm-core/src/types/legacy.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* @fileoverview Legacy type definitions for backwards compatibility
|
||||
* These types are deprecated and will be removed in future versions
|
||||
*/
|
||||
|
||||
/**
|
||||
* @deprecated Use string directly instead. This will be removed in a future version.
|
||||
*/
|
||||
export type TaskId = string;
|
||||
142
packages/tm-core/src/utils/id-generator.ts
Normal file
142
packages/tm-core/src/utils/id-generator.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* @fileoverview ID generation utilities for Task Master
|
||||
* Provides functions to generate unique identifiers for tasks and subtasks
|
||||
*/
|
||||
|
||||
import { randomBytes } from 'crypto';
|
||||
|
||||
/**
|
||||
* Generates a unique task ID using the format: TASK-{timestamp}-{random}
|
||||
*
|
||||
* @returns A unique task ID string
|
||||
* @example
|
||||
* ```typescript
|
||||
* const taskId = generateTaskId();
|
||||
* // Returns something like: "TASK-1704067200000-A7B3"
|
||||
* ```
|
||||
*/
|
||||
export function generateTaskId(): string {
|
||||
const timestamp = Date.now();
|
||||
const random = generateRandomString(4);
|
||||
return `TASK-${timestamp}-${random}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a subtask ID using the format: {parentId}.{sequential}
|
||||
*
|
||||
* @param parentId - The ID of the parent task
|
||||
* @param existingSubtasks - Array of existing subtask IDs to determine the next sequential number
|
||||
* @returns A unique subtask ID string
|
||||
* @example
|
||||
* ```typescript
|
||||
* const subtaskId = generateSubtaskId("TASK-123-A7B3", ["TASK-123-A7B3.1"]);
|
||||
* // Returns: "TASK-123-A7B3.2"
|
||||
* ```
|
||||
*/
|
||||
export function generateSubtaskId(
|
||||
parentId: string,
|
||||
existingSubtasks: string[] = []
|
||||
): string {
|
||||
// Find existing subtasks for this parent
|
||||
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);
|
||||
})
|
||||
.filter((num) => !isNaN(num))
|
||||
.sort((a, b) => a - b);
|
||||
|
||||
// Determine the next sequential number
|
||||
const nextSequential =
|
||||
sequentialNumbers.length > 0 ? Math.max(...sequentialNumbers) + 1 : 1;
|
||||
|
||||
return `${parentId}.${nextSequential}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random alphanumeric string of specified length
|
||||
* Uses crypto.randomBytes for cryptographically secure randomness
|
||||
*
|
||||
* @param length - The desired length of the random string
|
||||
* @returns A random alphanumeric string
|
||||
* @internal
|
||||
*/
|
||||
function generateRandomString(length: number): string {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
const bytes = randomBytes(length);
|
||||
let result = '';
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars[bytes[i] % chars.length];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a task ID format
|
||||
*
|
||||
* @param id - The ID to validate
|
||||
* @returns True if the ID matches the expected task ID format
|
||||
* @example
|
||||
* ```typescript
|
||||
* isValidTaskId("TASK-1704067200000-A7B3"); // true
|
||||
* isValidTaskId("invalid-id"); // false
|
||||
* ```
|
||||
*/
|
||||
export function isValidTaskId(id: string): boolean {
|
||||
const taskIdRegex = /^TASK-\d{13}-[A-Z0-9]{4}$/;
|
||||
return taskIdRegex.test(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a subtask ID format
|
||||
*
|
||||
* @param id - The ID to validate
|
||||
* @returns True if the ID matches the expected subtask ID format
|
||||
* @example
|
||||
* ```typescript
|
||||
* isValidSubtaskId("TASK-1704067200000-A7B3.1"); // true
|
||||
* isValidSubtaskId("TASK-1704067200000-A7B3.1.2"); // true (nested subtask)
|
||||
* isValidSubtaskId("invalid.id"); // false
|
||||
* ```
|
||||
*/
|
||||
export function isValidSubtaskId(id: string): boolean {
|
||||
const parts = id.split('.');
|
||||
if (parts.length < 2) return false;
|
||||
|
||||
// First part should be a valid task ID
|
||||
const taskIdPart = parts[0];
|
||||
if (!isValidTaskId(taskIdPart)) return false;
|
||||
|
||||
// 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();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the parent task ID from a subtask ID
|
||||
*
|
||||
* @param subtaskId - The subtask ID
|
||||
* @returns The parent task ID, or null if the input is not a valid subtask ID
|
||||
* @example
|
||||
* ```typescript
|
||||
* getParentTaskId("TASK-1704067200000-A7B3.1.2"); // "TASK-1704067200000-A7B3"
|
||||
* getParentTaskId("TASK-1704067200000-A7B3"); // null (not a subtask)
|
||||
* ```
|
||||
*/
|
||||
export function getParentTaskId(subtaskId: string): string | null {
|
||||
if (!isValidSubtaskId(subtaskId)) return null;
|
||||
|
||||
const parts = subtaskId.split('.');
|
||||
return parts[0];
|
||||
}
|
||||
44
packages/tm-core/src/utils/index.ts
Normal file
44
packages/tm-core/src/utils/index.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* @fileoverview Utility functions for the tm-core package
|
||||
* This file exports all utility functions and helper classes
|
||||
*/
|
||||
|
||||
// Utility implementations will be defined here
|
||||
// export * from './validation.js';
|
||||
// export * from './formatting.js';
|
||||
// export * from './file-utils.js';
|
||||
// export * from './async-utils.js';
|
||||
|
||||
// Placeholder exports - these will be implemented in later tasks
|
||||
|
||||
/**
|
||||
* Generates a unique ID for tasks
|
||||
* @deprecated This is a placeholder function that will be properly implemented in later tasks
|
||||
*/
|
||||
export function generateTaskId(): string {
|
||||
return `task-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a task ID format
|
||||
* @deprecated This is a placeholder function that will be properly implemented in later tasks
|
||||
*/
|
||||
export function isValidTaskId(id: string): boolean {
|
||||
return typeof id === 'string' && id.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a date for task timestamps
|
||||
* @deprecated This is a placeholder function that will be properly implemented in later tasks
|
||||
*/
|
||||
export function formatDate(date: Date = new Date()): string {
|
||||
return date.toISOString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep clones an object
|
||||
* @deprecated This is a placeholder function that will be properly implemented in later tasks
|
||||
*/
|
||||
export function deepClone<T>(obj: T): T {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
}
|
||||
22
packages/tm-core/tests/setup.ts
Normal file
22
packages/tm-core/tests/setup.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Jest setup file for tm-core package
|
||||
* This file is executed before running tests and can be used to configure
|
||||
* testing utilities, global mocks, and test environment setup.
|
||||
*/
|
||||
|
||||
// Configure test environment
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
// Global test utilities can be added here
|
||||
// Custom matchers and global types can be defined here in the future
|
||||
|
||||
// Set up any global mocks or configurations here
|
||||
beforeEach(() => {
|
||||
// Reset any global state before each test
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up after each test
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
150
packages/tm-core/tests/unit/smoke.test.ts
Normal file
150
packages/tm-core/tests/unit/smoke.test.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* Smoke tests to verify basic package functionality and imports
|
||||
*/
|
||||
|
||||
import {
|
||||
generateTaskId,
|
||||
isValidTaskId,
|
||||
formatDate,
|
||||
PlaceholderProvider,
|
||||
PlaceholderStorage,
|
||||
PlaceholderParser,
|
||||
TmCoreError,
|
||||
TaskNotFoundError,
|
||||
ValidationError,
|
||||
StorageError,
|
||||
version,
|
||||
name
|
||||
} from '@/index';
|
||||
|
||||
import type {
|
||||
TaskId,
|
||||
TaskStatus,
|
||||
TaskPriority,
|
||||
PlaceholderTask
|
||||
} from '@/types/index';
|
||||
|
||||
describe('tm-core smoke tests', () => {
|
||||
describe('package metadata', () => {
|
||||
it('should export correct package name and version', () => {
|
||||
expect(name).toBe('@task-master/tm-core');
|
||||
expect(version).toBe('1.0.0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('utility functions', () => {
|
||||
it('should generate valid task IDs', () => {
|
||||
const id1 = generateTaskId();
|
||||
const id2 = generateTaskId();
|
||||
|
||||
expect(typeof id1).toBe('string');
|
||||
expect(typeof id2).toBe('string');
|
||||
expect(id1).not.toBe(id2); // Should be unique
|
||||
expect(isValidTaskId(id1)).toBe(true);
|
||||
expect(isValidTaskId('')).toBe(false);
|
||||
});
|
||||
|
||||
it('should format dates', () => {
|
||||
const date = new Date('2023-01-01T00:00:00.000Z');
|
||||
const formatted = formatDate(date);
|
||||
expect(formatted).toBe('2023-01-01T00:00:00.000Z');
|
||||
});
|
||||
});
|
||||
|
||||
describe('placeholder provider', () => {
|
||||
it('should create and use placeholder provider', async () => {
|
||||
const provider = new PlaceholderProvider();
|
||||
expect(provider.name).toBe('placeholder');
|
||||
|
||||
const response = await provider.generateResponse('test prompt');
|
||||
expect(response).toContain('test prompt');
|
||||
});
|
||||
});
|
||||
|
||||
describe('placeholder storage', () => {
|
||||
it('should perform basic storage operations', async () => {
|
||||
const storage = new PlaceholderStorage();
|
||||
const testPath = 'test/path';
|
||||
const testData = 'test data';
|
||||
|
||||
// Initially should not exist
|
||||
expect(await storage.exists(testPath)).toBe(false);
|
||||
expect(await storage.read(testPath)).toBe(null);
|
||||
|
||||
// Write and verify
|
||||
await storage.write(testPath, testData);
|
||||
expect(await storage.exists(testPath)).toBe(true);
|
||||
expect(await storage.read(testPath)).toBe(testData);
|
||||
|
||||
// Delete and verify
|
||||
await storage.delete(testPath);
|
||||
expect(await storage.exists(testPath)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('placeholder parser', () => {
|
||||
it('should parse simple task lists', async () => {
|
||||
const parser = new PlaceholderParser();
|
||||
const content = `
|
||||
- Task 1
|
||||
- Task 2
|
||||
- Task 3
|
||||
`;
|
||||
|
||||
const isValid = await parser.validate(content);
|
||||
expect(isValid).toBe(true);
|
||||
|
||||
const tasks = await parser.parse(content);
|
||||
expect(tasks).toHaveLength(3);
|
||||
expect(tasks[0]?.title).toBe('Task 1');
|
||||
expect(tasks[1]?.title).toBe('Task 2');
|
||||
expect(tasks[2]?.title).toBe('Task 3');
|
||||
|
||||
tasks.forEach((task) => {
|
||||
expect(task.status).toBe('pending');
|
||||
expect(task.priority).toBe('medium');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('error classes', () => {
|
||||
it('should create and throw custom errors', () => {
|
||||
const baseError = new TmCoreError('Base error');
|
||||
expect(baseError.name).toBe('TmCoreError');
|
||||
expect(baseError.message).toBe('Base error');
|
||||
|
||||
const taskNotFound = new TaskNotFoundError('task-123');
|
||||
expect(taskNotFound.name).toBe('TaskNotFoundError');
|
||||
expect(taskNotFound.code).toBe('TASK_NOT_FOUND');
|
||||
expect(taskNotFound.message).toContain('task-123');
|
||||
|
||||
const validationError = new ValidationError('Invalid data');
|
||||
expect(validationError.name).toBe('ValidationError');
|
||||
expect(validationError.code).toBe('VALIDATION_ERROR');
|
||||
|
||||
const storageError = new StorageError('Storage failed');
|
||||
expect(storageError.name).toBe('StorageError');
|
||||
expect(storageError.code).toBe('STORAGE_ERROR');
|
||||
});
|
||||
});
|
||||
|
||||
describe('type definitions', () => {
|
||||
it('should have correct types available', () => {
|
||||
// These are compile-time checks that verify types exist
|
||||
const taskId: TaskId = 'test-id';
|
||||
const status: TaskStatus = 'pending';
|
||||
const priority: TaskPriority = 'high';
|
||||
|
||||
const task: PlaceholderTask = {
|
||||
id: taskId,
|
||||
title: 'Test Task',
|
||||
status: status,
|
||||
priority: priority
|
||||
};
|
||||
|
||||
expect(task.id).toBe('test-id');
|
||||
expect(task.status).toBe('pending');
|
||||
expect(task.priority).toBe('high');
|
||||
});
|
||||
});
|
||||
});
|
||||
41
packages/tm-core/tsconfig.json
Normal file
41
packages/tm-core/tsconfig.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2022"],
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"strictBindCallApply": true,
|
||||
"strictPropertyInitialization": true,
|
||||
"noImplicitThis": true,
|
||||
"alwaysStrict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@/types": ["./src/types/index"],
|
||||
"@/providers": ["./src/providers/index"],
|
||||
"@/storage": ["./src/storage/index"],
|
||||
"@/parser": ["./src/parser/index"],
|
||||
"@/utils": ["./src/utils/index"],
|
||||
"@/errors": ["./src/errors/index"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "tests", "**/*.test.ts", "**/*.spec.ts"]
|
||||
}
|
||||
27
packages/tm-core/tsup.config.ts
Normal file
27
packages/tm-core/tsup.config.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
|
||||
export default defineConfig({
|
||||
entry: {
|
||||
index: 'src/index.ts',
|
||||
'types/index': 'src/types/index.ts',
|
||||
'providers/index': 'src/providers/index.ts',
|
||||
'storage/index': 'src/storage/index.ts',
|
||||
'parser/index': 'src/parser/index.ts',
|
||||
'utils/index': 'src/utils/index.ts',
|
||||
'errors/index': 'src/errors/index.ts'
|
||||
},
|
||||
format: ['cjs', 'esm'],
|
||||
dts: true,
|
||||
sourcemap: true,
|
||||
clean: true,
|
||||
splitting: false,
|
||||
treeshake: true,
|
||||
minify: false,
|
||||
target: 'es2022',
|
||||
tsconfig: './tsconfig.json',
|
||||
outDir: 'dist',
|
||||
external: ['zod'],
|
||||
esbuildOptions(options) {
|
||||
options.conditions = ['module'];
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user