mirror of
https://github.com/eyaltoledano/claude-task-master.git
synced 2026-01-30 06:12:05 +00:00
Merge pull request #1348 from eyaltoledano/next (Release 0.30.1)
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
---
|
||||
"@tm/cli": patch
|
||||
"task-master-ai": patch
|
||||
---
|
||||
|
||||
Fix warning message box width to match dashboard box width for consistent UI alignment
|
||||
|
||||
7
.changeset/four-bugs-occur.md
Normal file
7
.changeset/four-bugs-occur.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"task-master-ai": patch
|
||||
---
|
||||
|
||||
remove file and complexity report parameter from get-tasks and get-task mcp tool
|
||||
|
||||
- In an effort to reduce complexity and context bloat for ai coding agents, we simplified the parameters of these tools
|
||||
13
.changeset/pre.json
Normal file
13
.changeset/pre.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"mode": "exit",
|
||||
"tag": "rc",
|
||||
"initialVersions": {
|
||||
"task-master-ai": "0.30.0",
|
||||
"docs": "0.0.7",
|
||||
"extension": "0.25.6",
|
||||
"@tm/claude-code-plugin": "0.0.2"
|
||||
},
|
||||
"changesets": [
|
||||
"fix-warning-box-alignment"
|
||||
]
|
||||
}
|
||||
13
.github/workflows/extension-ci.yml
vendored
13
.github/workflows/extension-ci.yml
vendored
@@ -6,15 +6,15 @@ on:
|
||||
- main
|
||||
- next
|
||||
paths:
|
||||
- 'apps/extension/**'
|
||||
- '.github/workflows/extension-ci.yml'
|
||||
- "apps/extension/**"
|
||||
- ".github/workflows/extension-ci.yml"
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- next
|
||||
paths:
|
||||
- 'apps/extension/**'
|
||||
- '.github/workflows/extension-ci.yml'
|
||||
- "apps/extension/**"
|
||||
- ".github/workflows/extension-ci.yml"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -55,7 +55,6 @@ jobs:
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
|
||||
- name: Restore node_modules
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
@@ -72,7 +71,7 @@ jobs:
|
||||
|
||||
- name: Type Check Extension
|
||||
working-directory: apps/extension
|
||||
run: npm run check-types
|
||||
run: npm run typecheck
|
||||
env:
|
||||
FORCE_COLOR: 1
|
||||
|
||||
@@ -86,7 +85,6 @@ jobs:
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
|
||||
- name: Restore node_modules
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
@@ -137,4 +135,3 @@ jobs:
|
||||
apps/extension/vsix-build/*.vsix
|
||||
apps/extension/dist/
|
||||
retention-days: 30
|
||||
|
||||
|
||||
4
.github/workflows/extension-release.yml
vendored
4
.github/workflows/extension-release.yml
vendored
@@ -37,7 +37,7 @@ jobs:
|
||||
|
||||
- name: Type Check Extension
|
||||
working-directory: apps/extension
|
||||
run: npm run check-types
|
||||
run: npm run typecheck
|
||||
env:
|
||||
FORCE_COLOR: 1
|
||||
|
||||
@@ -107,4 +107,4 @@ jobs:
|
||||
echo "🎉 Extension ${{ github.ref_name }} successfully published!"
|
||||
echo "📦 Available on VS Code Marketplace"
|
||||
echo "🌍 Available on Open VSX Registry"
|
||||
echo "🏷️ GitHub release created: ${{ github.ref_name }}"
|
||||
echo "🏷️ GitHub release created: ${{ github.ref_name }}"
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
{
|
||||
"models": {
|
||||
"main": {
|
||||
"provider": "claude-code",
|
||||
"modelId": "sonnet",
|
||||
"provider": "anthropic",
|
||||
"modelId": "claude-sonnet-4-5-20250929",
|
||||
"maxTokens": 64000,
|
||||
"temperature": 0.2
|
||||
},
|
||||
"research": {
|
||||
"provider": "perplexity",
|
||||
"modelId": "sonar",
|
||||
"modelId": "sonar-pro",
|
||||
"maxTokens": 8700,
|
||||
"temperature": 0.1
|
||||
},
|
||||
"fallback": {
|
||||
"provider": "anthropic",
|
||||
"modelId": "claude-3-7-sonnet-20250219",
|
||||
"maxTokens": 120000,
|
||||
"modelId": "claude-sonnet-4-20250514",
|
||||
"maxTokens": 64000,
|
||||
"temperature": 0.2
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"currentTag": "tdd-phase-1-core-rails",
|
||||
"lastSwitched": "2025-10-09T12:41:40.367Z",
|
||||
"currentTag": "master",
|
||||
"lastSwitched": "2025-10-27T09:28:03.574Z",
|
||||
"branchTagMapping": {
|
||||
"v017-adds": "v017-adds",
|
||||
"next": "next"
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# task-master-ai
|
||||
|
||||
## 0.30.1-rc.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#1305](https://github.com/eyaltoledano/claude-task-master/pull/1305) [`a98d96e`](https://github.com/eyaltoledano/claude-task-master/commit/a98d96ef0414833b948672f86da4acc11f700ebb) Thanks [@bjcoombs](https://github.com/bjcoombs)! - Fix warning message box width to match dashboard box width for consistent UI alignment
|
||||
|
||||
## 0.30.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
42
CLAUDE.md
42
CLAUDE.md
@@ -33,6 +33,48 @@
|
||||
});
|
||||
```
|
||||
|
||||
## Architecture Guidelines
|
||||
|
||||
### Business Logic Separation
|
||||
|
||||
**CRITICAL RULE**: ALL business logic must live in `@tm/core`, NOT in presentation layers.
|
||||
|
||||
- **`@tm/core`** (packages/tm-core/):
|
||||
- Contains ALL business logic, domain models, services, and utilities
|
||||
- Provides clean facade APIs through domain objects (tasks, auth, workflow, git, config)
|
||||
- Houses all complexity - parsing, validation, transformations, calculations, etc.
|
||||
- Example: Task ID parsing, subtask extraction, status validation, dependency resolution
|
||||
|
||||
- **`@tm/cli`** (apps/cli/):
|
||||
- Thin presentation layer ONLY
|
||||
- Calls tm-core methods and displays results
|
||||
- Handles CLI-specific concerns: argument parsing, output formatting, user prompts
|
||||
- NO business logic, NO data transformations, NO calculations
|
||||
|
||||
- **`@tm/mcp`** (apps/mcp/):
|
||||
- Thin presentation layer ONLY
|
||||
- Calls tm-core methods and returns MCP-formatted responses
|
||||
- Handles MCP-specific concerns: tool schemas, parameter validation, response formatting
|
||||
- NO business logic, NO data transformations, NO calculations
|
||||
|
||||
- **`apps/extension`** (future):
|
||||
- Thin presentation layer ONLY
|
||||
- Calls tm-core methods and displays in VS Code UI
|
||||
- NO business logic
|
||||
|
||||
**Examples of violations to avoid:**
|
||||
|
||||
- ❌ Creating helper functions in CLI/MCP to parse task IDs → Move to tm-core
|
||||
- ❌ Data transformation logic in CLI/MCP → Move to tm-core
|
||||
- ❌ Validation logic in CLI/MCP → Move to tm-core
|
||||
- ❌ Duplicating logic across CLI and MCP → Implement once in tm-core
|
||||
|
||||
**Correct approach:**
|
||||
- ✅ Add method to TasksDomain: `tasks.get(taskId)` (automatically handles task and subtask IDs)
|
||||
- ✅ CLI calls: `await tmCore.tasks.get(taskId)` (supports "1", "1.2", "HAM-123", "HAM-123.2")
|
||||
- ✅ MCP calls: `await tmCore.tasks.get(taskId)` (same intelligent ID parsing)
|
||||
- ✅ Single source of truth in tm-core
|
||||
|
||||
## Documentation Guidelines
|
||||
|
||||
- **Documentation location**: Write docs in `apps/docs/` (Mintlify site source), not `docs/`
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
AuthManager,
|
||||
AuthenticationError,
|
||||
type AuthCredentials
|
||||
} from '@tm/core/auth';
|
||||
} from '@tm/core';
|
||||
import * as ui from '../utils/ui.js';
|
||||
import { ContextCommand } from './context.command.js';
|
||||
import { displayError } from '../utils/error-handler.js';
|
||||
|
||||
@@ -8,12 +8,7 @@ import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import boxen from 'boxen';
|
||||
import ora, { type Ora } from 'ora';
|
||||
import {
|
||||
createTaskMasterCore,
|
||||
type TaskMasterCore,
|
||||
type Task,
|
||||
type Subtask
|
||||
} from '@tm/core';
|
||||
import { createTmCore, type TmCore, type Task, type Subtask } from '@tm/core';
|
||||
import * as ui from '../utils/ui.js';
|
||||
|
||||
/**
|
||||
@@ -60,7 +55,7 @@ export interface AutopilotCommandResult {
|
||||
* This is a thin presentation layer over @tm/core's autopilot functionality
|
||||
*/
|
||||
export class AutopilotCommand extends Command {
|
||||
private tmCore?: TaskMasterCore;
|
||||
private tmCore?: TmCore;
|
||||
private lastResult?: AutopilotCommandResult;
|
||||
|
||||
constructor(name?: string) {
|
||||
@@ -164,11 +159,11 @@ export class AutopilotCommand extends Command {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize TaskMasterCore
|
||||
* Initialize TmCore
|
||||
*/
|
||||
private async initializeCore(projectRoot: string): Promise<void> {
|
||||
if (!this.tmCore) {
|
||||
this.tmCore = await createTaskMasterCore({ projectPath: projectRoot });
|
||||
this.tmCore = await createTmCore({ projectPath: projectRoot });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,11 +172,11 @@ export class AutopilotCommand extends Command {
|
||||
*/
|
||||
private async loadTask(taskId: string): Promise<Task | null> {
|
||||
if (!this.tmCore) {
|
||||
throw new Error('TaskMasterCore not initialized');
|
||||
throw new Error('TmCore not initialized');
|
||||
}
|
||||
|
||||
try {
|
||||
const { task } = await this.tmCore.getTaskWithSubtask(taskId);
|
||||
const { task } = await this.tmCore.tasks.get(taskId);
|
||||
return task;
|
||||
} catch (error) {
|
||||
return null;
|
||||
@@ -236,11 +231,7 @@ export class AutopilotCommand extends Command {
|
||||
}
|
||||
|
||||
// Validate task structure and get execution order
|
||||
const validationResult = await this.validateTaskStructure(
|
||||
taskId,
|
||||
task,
|
||||
options
|
||||
);
|
||||
const validationResult = await this.validateTaskStructure(taskId, task);
|
||||
if (!validationResult.success) {
|
||||
return validationResult;
|
||||
}
|
||||
@@ -288,19 +279,23 @@ export class AutopilotCommand extends Command {
|
||||
*/
|
||||
private async validateTaskStructure(
|
||||
taskId: string,
|
||||
task: Task,
|
||||
options: AutopilotCommandOptions
|
||||
task: Task
|
||||
): Promise<AutopilotCommandResult & { orderedSubtasks?: Subtask[] }> {
|
||||
const { TaskLoaderService } = await import('@tm/core');
|
||||
if (!this.tmCore) {
|
||||
return {
|
||||
success: false,
|
||||
taskId,
|
||||
task,
|
||||
error: 'TmCore not initialized'
|
||||
};
|
||||
}
|
||||
|
||||
console.log();
|
||||
console.log(chalk.cyan.bold('Validating task structure...'));
|
||||
|
||||
const taskLoader = new TaskLoaderService(options.project || process.cwd());
|
||||
const validationResult = await taskLoader.loadAndValidateTask(taskId);
|
||||
const validationResult = await this.tmCore.tasks.loadAndValidate(taskId);
|
||||
|
||||
if (!validationResult.success) {
|
||||
await taskLoader.cleanup();
|
||||
return {
|
||||
success: false,
|
||||
taskId,
|
||||
@@ -310,12 +305,10 @@ export class AutopilotCommand extends Command {
|
||||
};
|
||||
}
|
||||
|
||||
const orderedSubtasks = taskLoader.getExecutionOrder(
|
||||
const orderedSubtasks = this.tmCore.tasks.getExecutionOrder(
|
||||
validationResult.task!
|
||||
);
|
||||
|
||||
await taskLoader.cleanup();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
taskId,
|
||||
@@ -499,7 +492,6 @@ export class AutopilotCommand extends Command {
|
||||
*/
|
||||
async cleanup(): Promise<void> {
|
||||
if (this.tmCore) {
|
||||
await this.tmCore.close();
|
||||
this.tmCore = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
import { Command } from 'commander';
|
||||
import { createTaskMasterCore, type WorkflowContext } from '@tm/core';
|
||||
import { createTmCore, type WorkflowContext } from '@tm/core';
|
||||
import {
|
||||
AutopilotBaseOptions,
|
||||
hasWorkflowState,
|
||||
@@ -67,20 +67,19 @@ export class StartCommand extends Command {
|
||||
}
|
||||
|
||||
// Initialize Task Master Core
|
||||
const tmCore = await createTaskMasterCore({
|
||||
const tmCore = await createTmCore({
|
||||
projectPath: mergedOptions.projectRoot!
|
||||
});
|
||||
|
||||
// Get current tag from ConfigManager
|
||||
const currentTag = tmCore.getActiveTag();
|
||||
const currentTag = tmCore.config.getActiveTag();
|
||||
|
||||
// Load task
|
||||
formatter.info(`Loading task ${taskId}...`);
|
||||
const { task } = await tmCore.getTaskWithSubtask(taskId);
|
||||
const { task } = await tmCore.tasks.get(taskId);
|
||||
|
||||
if (!task) {
|
||||
formatter.error('Task not found', { taskId });
|
||||
await tmCore.close();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -90,7 +89,6 @@ export class StartCommand extends Command {
|
||||
taskId,
|
||||
suggestion: `Run: task-master expand --id=${taskId}`
|
||||
});
|
||||
await tmCore.close();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -156,7 +154,6 @@ export class StartCommand extends Command {
|
||||
});
|
||||
|
||||
// Clean up
|
||||
await tmCore.close();
|
||||
} catch (error) {
|
||||
formatter.error((error as Error).message);
|
||||
if (mergedOptions.verbose) {
|
||||
|
||||
@@ -8,7 +8,7 @@ import chalk from 'chalk';
|
||||
import inquirer from 'inquirer';
|
||||
import search from '@inquirer/search';
|
||||
import ora, { Ora } from 'ora';
|
||||
import { AuthManager, type UserContext } from '@tm/core/auth';
|
||||
import { AuthManager, type UserContext } from '@tm/core';
|
||||
import * as ui from '../utils/ui.js';
|
||||
import { displayError } from '../utils/error-handler.js';
|
||||
|
||||
|
||||
@@ -7,8 +7,13 @@ import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import inquirer from 'inquirer';
|
||||
import ora, { Ora } from 'ora';
|
||||
import { AuthManager, type UserContext } from '@tm/core/auth';
|
||||
import { TaskMasterCore, type ExportResult } from '@tm/core';
|
||||
import {
|
||||
AuthManager,
|
||||
type UserContext,
|
||||
type ExportResult,
|
||||
createTmCore,
|
||||
type TmCore
|
||||
} from '@tm/core';
|
||||
import * as ui from '../utils/ui.js';
|
||||
import { displayError } from '../utils/error-handler.js';
|
||||
|
||||
@@ -28,7 +33,7 @@ export interface ExportCommandResult {
|
||||
*/
|
||||
export class ExportCommand extends Command {
|
||||
private authManager: AuthManager;
|
||||
private taskMasterCore?: TaskMasterCore;
|
||||
private taskMasterCore?: TmCore;
|
||||
private lastResult?: ExportCommandResult;
|
||||
|
||||
constructor(name?: string) {
|
||||
@@ -61,7 +66,7 @@ export class ExportCommand extends Command {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the TaskMasterCore
|
||||
* Initialize the TmCore
|
||||
*/
|
||||
private async initializeServices(): Promise<void> {
|
||||
if (this.taskMasterCore) {
|
||||
@@ -69,8 +74,8 @@ export class ExportCommand extends Command {
|
||||
}
|
||||
|
||||
try {
|
||||
// Initialize TaskMasterCore
|
||||
this.taskMasterCore = await TaskMasterCore.create({
|
||||
// Initialize TmCore
|
||||
this.taskMasterCore = await createTmCore({
|
||||
projectPath: process.cwd()
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -152,7 +157,8 @@ export class ExportCommand extends Command {
|
||||
// Perform export
|
||||
spinner = ora('Exporting tasks...').start();
|
||||
|
||||
const exportResult = await this.taskMasterCore!.exportTasks({
|
||||
// Use integration domain facade
|
||||
const exportResult = await this.taskMasterCore!.integration.exportTasks({
|
||||
orgId,
|
||||
briefId,
|
||||
tag: options?.tag,
|
||||
|
||||
@@ -6,16 +6,16 @@
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import {
|
||||
createTaskMasterCore,
|
||||
createTmCore,
|
||||
type Task,
|
||||
type TaskStatus,
|
||||
type TaskMasterCore,
|
||||
type TmCore,
|
||||
TASK_STATUSES,
|
||||
OUTPUT_FORMATS,
|
||||
STATUS_ICONS,
|
||||
type OutputFormat
|
||||
} from '@tm/core';
|
||||
import type { StorageType } from '@tm/core/types';
|
||||
import type { StorageType } from '@tm/core';
|
||||
import * as ui from '../utils/ui.js';
|
||||
import { displayError } from '../utils/error-handler.js';
|
||||
import { displayCommandHeader } from '../utils/display-helpers.js';
|
||||
@@ -59,7 +59,7 @@ export interface ListTasksResult {
|
||||
* This is a thin presentation layer over @tm/core
|
||||
*/
|
||||
export class ListTasksCommand extends Command {
|
||||
private tmCore?: TaskMasterCore;
|
||||
private tmCore?: TmCore;
|
||||
private lastResult?: ListTasksResult;
|
||||
|
||||
constructor(name?: string) {
|
||||
@@ -144,11 +144,11 @@ export class ListTasksCommand extends Command {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize TaskMasterCore
|
||||
* Initialize TmCore
|
||||
*/
|
||||
private async initializeCore(projectRoot: string): Promise<void> {
|
||||
if (!this.tmCore) {
|
||||
this.tmCore = await createTaskMasterCore({ projectPath: projectRoot });
|
||||
this.tmCore = await createTmCore({ projectPath: projectRoot });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ export class ListTasksCommand extends Command {
|
||||
options: ListCommandOptions
|
||||
): Promise<ListTasksResult> {
|
||||
if (!this.tmCore) {
|
||||
throw new Error('TaskMasterCore not initialized');
|
||||
throw new Error('TmCore not initialized');
|
||||
}
|
||||
|
||||
// Build filter
|
||||
@@ -173,7 +173,7 @@ export class ListTasksCommand extends Command {
|
||||
: undefined;
|
||||
|
||||
// Call tm-core
|
||||
const result = await this.tmCore.getTaskList({
|
||||
const result = await this.tmCore.tasks.list({
|
||||
tag: options.tag,
|
||||
filter,
|
||||
includeSubtasks: options.withSubtasks
|
||||
@@ -459,7 +459,6 @@ export class ListTasksCommand extends Command {
|
||||
*/
|
||||
async cleanup(): Promise<void> {
|
||||
if (this.tmCore) {
|
||||
await this.tmCore.close();
|
||||
this.tmCore = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ import path from 'node:path';
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import boxen from 'boxen';
|
||||
import { createTaskMasterCore, type Task, type TaskMasterCore } from '@tm/core';
|
||||
import type { StorageType } from '@tm/core/types';
|
||||
import { createTmCore, type Task, type TmCore } from '@tm/core';
|
||||
import type { StorageType } from '@tm/core';
|
||||
import { displayError } from '../utils/error-handler.js';
|
||||
import { displayTaskDetails } from '../ui/components/task-detail.component.js';
|
||||
import { displayCommandHeader } from '../utils/display-helpers.js';
|
||||
@@ -38,7 +38,7 @@ export interface NextTaskResult {
|
||||
* This is a thin presentation layer over @tm/core
|
||||
*/
|
||||
export class NextCommand extends Command {
|
||||
private tmCore?: TaskMasterCore;
|
||||
private tmCore?: TmCore;
|
||||
private lastResult?: NextTaskResult;
|
||||
|
||||
constructor(name?: string) {
|
||||
@@ -104,12 +104,12 @@ export class NextCommand extends Command {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize TaskMasterCore
|
||||
* Initialize TmCore
|
||||
*/
|
||||
private async initializeCore(projectRoot: string): Promise<void> {
|
||||
if (!this.tmCore) {
|
||||
const resolved = path.resolve(projectRoot);
|
||||
this.tmCore = await createTaskMasterCore({ projectPath: resolved });
|
||||
this.tmCore = await createTmCore({ projectPath: resolved });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,18 +120,16 @@ export class NextCommand extends Command {
|
||||
options: NextCommandOptions
|
||||
): Promise<NextTaskResult> {
|
||||
if (!this.tmCore) {
|
||||
throw new Error('TaskMasterCore not initialized');
|
||||
throw new Error('TmCore not initialized');
|
||||
}
|
||||
|
||||
// Call tm-core to get next task
|
||||
const task = await this.tmCore.getNextTask(options.tag);
|
||||
const task = await this.tmCore.tasks.getNext(options.tag);
|
||||
|
||||
// Get storage type and active tag
|
||||
const storageType = this.tmCore.getStorageType();
|
||||
if (storageType === 'auto') {
|
||||
throw new Error('Storage type must be resolved before use');
|
||||
}
|
||||
const activeTag = options.tag || this.tmCore.getActiveTag();
|
||||
const storageType = this.tmCore.tasks.getStorageType();
|
||||
|
||||
const activeTag = options.tag || this.tmCore.config.getActiveTag();
|
||||
|
||||
return {
|
||||
task,
|
||||
@@ -232,7 +230,6 @@ export class NextCommand extends Command {
|
||||
*/
|
||||
async cleanup(): Promise<void> {
|
||||
if (this.tmCore) {
|
||||
await this.tmCore.close();
|
||||
this.tmCore = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,8 @@
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import boxen from 'boxen';
|
||||
import {
|
||||
createTaskMasterCore,
|
||||
type TaskMasterCore,
|
||||
type TaskStatus
|
||||
} from '@tm/core';
|
||||
import type { StorageType } from '@tm/core/types';
|
||||
import { createTmCore, type TmCore, type TaskStatus } from '@tm/core';
|
||||
import type { StorageType } from '@tm/core';
|
||||
import { displayError } from '../utils/error-handler.js';
|
||||
|
||||
/**
|
||||
@@ -56,7 +52,7 @@ export interface SetStatusResult {
|
||||
* This is a thin presentation layer over @tm/core
|
||||
*/
|
||||
export class SetStatusCommand extends Command {
|
||||
private tmCore?: TaskMasterCore;
|
||||
private tmCore?: TmCore;
|
||||
private lastResult?: SetStatusResult;
|
||||
|
||||
constructor(name?: string) {
|
||||
@@ -112,7 +108,7 @@ export class SetStatusCommand extends Command {
|
||||
}
|
||||
|
||||
// Initialize TaskMaster core
|
||||
this.tmCore = await createTaskMasterCore({
|
||||
this.tmCore = await createTmCore({
|
||||
projectPath: options.project || process.cwd()
|
||||
});
|
||||
|
||||
@@ -128,7 +124,7 @@ export class SetStatusCommand extends Command {
|
||||
|
||||
for (const taskId of taskIds) {
|
||||
try {
|
||||
const result = await this.tmCore.updateTaskStatus(
|
||||
const result = await this.tmCore.tasks.updateStatus(
|
||||
taskId,
|
||||
options.status
|
||||
);
|
||||
@@ -168,10 +164,7 @@ export class SetStatusCommand extends Command {
|
||||
this.lastResult = {
|
||||
success: true,
|
||||
updatedTasks,
|
||||
storageType: this.tmCore.getStorageType() as Exclude<
|
||||
StorageType,
|
||||
'auto'
|
||||
>
|
||||
storageType: this.tmCore.tasks.getStorageType()
|
||||
};
|
||||
|
||||
// Display results
|
||||
@@ -188,7 +181,6 @@ export class SetStatusCommand extends Command {
|
||||
} finally {
|
||||
// Clean up resources
|
||||
if (this.tmCore) {
|
||||
await this.tmCore.close();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import boxen from 'boxen';
|
||||
import { createTaskMasterCore, type Task, type TaskMasterCore } from '@tm/core';
|
||||
import type { StorageType } from '@tm/core/types';
|
||||
import { createTmCore, type Task, type TmCore } from '@tm/core';
|
||||
import type { StorageType } from '@tm/core';
|
||||
import * as ui from '../utils/ui.js';
|
||||
import { displayError } from '../utils/error-handler.js';
|
||||
import { displayTaskDetails } from '../ui/components/task-detail.component.js';
|
||||
@@ -47,7 +47,7 @@ export interface ShowMultipleTasksResult {
|
||||
* This is a thin presentation layer over @tm/core
|
||||
*/
|
||||
export class ShowCommand extends Command {
|
||||
private tmCore?: TaskMasterCore;
|
||||
private tmCore?: TmCore;
|
||||
private lastResult?: ShowTaskResult | ShowMultipleTasksResult;
|
||||
|
||||
constructor(name?: string) {
|
||||
@@ -133,11 +133,11 @@ export class ShowCommand extends Command {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize TaskMasterCore
|
||||
* Initialize TmCore
|
||||
*/
|
||||
private async initializeCore(projectRoot: string): Promise<void> {
|
||||
if (!this.tmCore) {
|
||||
this.tmCore = await createTaskMasterCore({ projectPath: projectRoot });
|
||||
this.tmCore = await createTmCore({ projectPath: projectRoot });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,18 +149,18 @@ export class ShowCommand extends Command {
|
||||
_options: ShowCommandOptions
|
||||
): Promise<ShowTaskResult> {
|
||||
if (!this.tmCore) {
|
||||
throw new Error('TaskMasterCore not initialized');
|
||||
throw new Error('TmCore not initialized');
|
||||
}
|
||||
|
||||
// Get the task
|
||||
const task = await this.tmCore.getTask(taskId);
|
||||
const result = await this.tmCore.tasks.get(taskId);
|
||||
|
||||
// Get storage type
|
||||
const storageType = this.tmCore.getStorageType();
|
||||
const storageType = this.tmCore.tasks.getStorageType();
|
||||
|
||||
return {
|
||||
task,
|
||||
found: task !== null,
|
||||
task: result.task,
|
||||
found: result.task !== null,
|
||||
storageType: storageType as Exclude<StorageType, 'auto'>
|
||||
};
|
||||
}
|
||||
@@ -173,7 +173,7 @@ export class ShowCommand extends Command {
|
||||
_options: ShowCommandOptions
|
||||
): Promise<ShowMultipleTasksResult> {
|
||||
if (!this.tmCore) {
|
||||
throw new Error('TaskMasterCore not initialized');
|
||||
throw new Error('TmCore not initialized');
|
||||
}
|
||||
|
||||
const tasks: Task[] = [];
|
||||
@@ -181,21 +181,21 @@ export class ShowCommand extends Command {
|
||||
|
||||
// Get each task individually
|
||||
for (const taskId of taskIds) {
|
||||
const task = await this.tmCore.getTask(taskId);
|
||||
if (task) {
|
||||
tasks.push(task);
|
||||
const result = await this.tmCore.tasks.get(taskId);
|
||||
if (result.task) {
|
||||
tasks.push(result.task);
|
||||
} else {
|
||||
notFound.push(taskId);
|
||||
}
|
||||
}
|
||||
|
||||
// Get storage type
|
||||
const storageType = this.tmCore.getStorageType();
|
||||
// Get storage type (resolved, not config value)
|
||||
const storageType = this.tmCore.tasks.getStorageType();
|
||||
|
||||
return {
|
||||
tasks,
|
||||
notFound,
|
||||
storageType: storageType as Exclude<StorageType, 'auto'>
|
||||
storageType
|
||||
};
|
||||
}
|
||||
|
||||
@@ -253,7 +253,7 @@ export class ShowCommand extends Command {
|
||||
}
|
||||
|
||||
// Display header with storage info
|
||||
const activeTag = this.tmCore?.getActiveTag() || 'master';
|
||||
const activeTag = this.tmCore?.config.getActiveTag() || 'master';
|
||||
displayCommandHeader(this.tmCore, {
|
||||
tag: activeTag,
|
||||
storageType: result.storageType
|
||||
@@ -276,7 +276,7 @@ export class ShowCommand extends Command {
|
||||
_options: ShowCommandOptions
|
||||
): void {
|
||||
// Display header with storage info
|
||||
const activeTag = this.tmCore?.getActiveTag() || 'master';
|
||||
const activeTag = this.tmCore?.config.getActiveTag() || 'master';
|
||||
displayCommandHeader(this.tmCore, {
|
||||
tag: activeTag,
|
||||
storageType: result.storageType
|
||||
@@ -322,7 +322,6 @@ export class ShowCommand extends Command {
|
||||
*/
|
||||
async cleanup(): Promise<void> {
|
||||
if (this.tmCore) {
|
||||
await this.tmCore.close();
|
||||
this.tmCore = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ import boxen from 'boxen';
|
||||
import ora, { type Ora } from 'ora';
|
||||
import { spawn } from 'child_process';
|
||||
import {
|
||||
createTaskMasterCore,
|
||||
type TaskMasterCore,
|
||||
createTmCore,
|
||||
type TmCore,
|
||||
type StartTaskResult as CoreStartTaskResult
|
||||
} from '@tm/core';
|
||||
import { displayTaskDetails } from '../ui/components/task-detail.component.js';
|
||||
@@ -43,7 +43,7 @@ export interface StartCommandResult extends CoreStartTaskResult {
|
||||
* This is a thin presentation layer over @tm/core's TaskExecutionService
|
||||
*/
|
||||
export class StartCommand extends Command {
|
||||
private tmCore?: TaskMasterCore;
|
||||
private tmCore?: TmCore;
|
||||
private lastResult?: StartCommandResult;
|
||||
|
||||
constructor(name?: string) {
|
||||
@@ -144,10 +144,10 @@ export class StartCommand extends Command {
|
||||
await this.executeChildProcess(coreResult.command);
|
||||
}
|
||||
|
||||
// Convert core result to CLI result with storage type
|
||||
// Convert core result to CLI result with storage type (resolved, not config value)
|
||||
const result: StartCommandResult = {
|
||||
...coreResult,
|
||||
storageType: this.tmCore?.getStorageType()
|
||||
storageType: this.tmCore?.tasks.getStorageType()
|
||||
};
|
||||
|
||||
// Store result for programmatic access
|
||||
@@ -180,11 +180,11 @@ export class StartCommand extends Command {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize TaskMasterCore
|
||||
* Initialize TmCore
|
||||
*/
|
||||
private async initializeCore(projectRoot: string): Promise<void> {
|
||||
if (!this.tmCore) {
|
||||
this.tmCore = await createTaskMasterCore({ projectPath: projectRoot });
|
||||
this.tmCore = await createTmCore({ projectPath: projectRoot });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,9 +193,9 @@ export class StartCommand extends Command {
|
||||
*/
|
||||
private async performGetNextTask(): Promise<string | null> {
|
||||
if (!this.tmCore) {
|
||||
throw new Error('TaskMasterCore not initialized');
|
||||
throw new Error('TmCore not initialized');
|
||||
}
|
||||
return this.tmCore.getNextAvailableTask();
|
||||
return this.tmCore.tasks.getNextAvailable();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -204,11 +204,10 @@ export class StartCommand extends Command {
|
||||
private async showPreLaunchMessage(targetTaskId: string): Promise<void> {
|
||||
if (!this.tmCore) return;
|
||||
|
||||
const { task, subtask, subtaskId } =
|
||||
await this.tmCore.getTaskWithSubtask(targetTaskId);
|
||||
const { task, isSubtask } = await this.tmCore.tasks.get(targetTaskId);
|
||||
if (task) {
|
||||
const workItemText = subtask
|
||||
? `Subtask #${task.id}.${subtaskId} - ${subtask.title}`
|
||||
const workItemText = isSubtask
|
||||
? `Subtask #${targetTaskId} - ${task.title}`
|
||||
: `Task #${task.id} - ${task.title}`;
|
||||
|
||||
console.log(
|
||||
@@ -227,7 +226,7 @@ export class StartCommand extends Command {
|
||||
options: StartCommandOptions
|
||||
): Promise<CoreStartTaskResult> {
|
||||
if (!this.tmCore) {
|
||||
throw new Error('TaskMasterCore not initialized');
|
||||
throw new Error('TmCore not initialized');
|
||||
}
|
||||
|
||||
// Show spinner for status update if enabled
|
||||
@@ -237,7 +236,7 @@ export class StartCommand extends Command {
|
||||
}
|
||||
|
||||
// Get execution command from tm-core (instead of executing directly)
|
||||
const result = await this.tmCore.startTask(targetTaskId, {
|
||||
const result = await this.tmCore.tasks.start(targetTaskId, {
|
||||
dryRun: options.dryRun,
|
||||
force: options.force,
|
||||
updateStatus: !options.noStatusUpdate
|
||||
@@ -471,7 +470,6 @@ export class StartCommand extends Command {
|
||||
*/
|
||||
async cleanup(): Promise<void> {
|
||||
if (this.tmCore) {
|
||||
await this.tmCore.close();
|
||||
this.tmCore = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,5 +41,5 @@ export type {
|
||||
Task,
|
||||
TaskStatus,
|
||||
TaskPriority,
|
||||
TaskMasterCore
|
||||
TmCore
|
||||
} from '@tm/core';
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import chalk from 'chalk';
|
||||
import boxen from 'boxen';
|
||||
import type { Task, TaskPriority } from '@tm/core/types';
|
||||
import type { Task, TaskPriority } from '@tm/core';
|
||||
import { getComplexityWithColor } from '../../utils/ui.js';
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import chalk from 'chalk';
|
||||
import boxen from 'boxen';
|
||||
import type { Task } from '@tm/core/types';
|
||||
import type { Task } from '@tm/core';
|
||||
import { getComplexityWithColor, getBoxWidth } from '../../utils/ui.js';
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,7 +8,7 @@ import boxen from 'boxen';
|
||||
import Table from 'cli-table3';
|
||||
import { marked, MarkedExtension } from 'marked';
|
||||
import { markedTerminal } from 'marked-terminal';
|
||||
import type { Task } from '@tm/core/types';
|
||||
import type { Task } from '@tm/core';
|
||||
import {
|
||||
getStatusWithColor,
|
||||
getPriorityWithColor,
|
||||
|
||||
@@ -3,73 +3,41 @@
|
||||
* Provides DRY utilities for displaying headers and other command output
|
||||
*/
|
||||
|
||||
import type { TaskMasterCore } from '@tm/core';
|
||||
import type { StorageType } from '@tm/core/types';
|
||||
import { displayHeader, type BriefInfo } from '../ui/index.js';
|
||||
|
||||
/**
|
||||
* Get web app base URL from environment
|
||||
*/
|
||||
function getWebAppUrl(): string | undefined {
|
||||
const baseDomain =
|
||||
process.env.TM_BASE_DOMAIN || process.env.TM_PUBLIC_BASE_DOMAIN;
|
||||
|
||||
if (!baseDomain) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// If it already includes protocol, use as-is
|
||||
if (baseDomain.startsWith('http://') || baseDomain.startsWith('https://')) {
|
||||
return baseDomain;
|
||||
}
|
||||
|
||||
// Otherwise, add protocol based on domain
|
||||
if (baseDomain.includes('localhost') || baseDomain.includes('127.0.0.1')) {
|
||||
return `http://${baseDomain}`;
|
||||
}
|
||||
|
||||
return `https://${baseDomain}`;
|
||||
}
|
||||
import type { TmCore } from '@tm/core';
|
||||
import type { StorageType } from '@tm/core';
|
||||
import { displayHeader } from '../ui/index.js';
|
||||
|
||||
/**
|
||||
* Display the command header with appropriate storage information
|
||||
* Handles both API and file storage displays
|
||||
*/
|
||||
export function displayCommandHeader(
|
||||
tmCore: TaskMasterCore | undefined,
|
||||
tmCore: TmCore | undefined,
|
||||
options: {
|
||||
tag?: string;
|
||||
storageType: Exclude<StorageType, 'auto'>;
|
||||
}
|
||||
): void {
|
||||
const { tag, storageType } = options;
|
||||
|
||||
// Get brief info if using API storage
|
||||
let briefInfo: BriefInfo | undefined;
|
||||
if (storageType === 'api' && tmCore) {
|
||||
const storageInfo = tmCore.getStorageDisplayInfo();
|
||||
if (storageInfo) {
|
||||
// Construct full brief info with web app URL
|
||||
briefInfo = {
|
||||
...storageInfo,
|
||||
webAppUrl: getWebAppUrl()
|
||||
};
|
||||
}
|
||||
if (!tmCore) {
|
||||
// Fallback display if tmCore is not available
|
||||
displayHeader({
|
||||
tag: options.tag || 'master',
|
||||
storageType: options.storageType
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Get file path for display (only for file storage)
|
||||
// Note: The file structure is fixed for file storage and won't change.
|
||||
// This is a display-only relative path, not used for actual file operations.
|
||||
const filePath =
|
||||
storageType === 'file' && tmCore
|
||||
? `.taskmaster/tasks/tasks.json`
|
||||
: undefined;
|
||||
// Get the resolved storage type from tasks domain
|
||||
const resolvedStorageType = tmCore.tasks.getStorageType();
|
||||
|
||||
// Display header
|
||||
// Get storage display info from tm-core (single source of truth)
|
||||
const displayInfo = tmCore.auth.getStorageDisplayInfo(resolvedStorageType);
|
||||
|
||||
// Display header with computed display info
|
||||
displayHeader({
|
||||
tag: tag || 'master',
|
||||
filePath: filePath,
|
||||
storageType: storageType === 'api' ? 'api' : 'file',
|
||||
briefInfo: briefInfo
|
||||
tag: options.tag || 'master',
|
||||
filePath: displayInfo.filePath,
|
||||
storageType: displayInfo.storageType,
|
||||
briefInfo: displayInfo.briefInfo
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import chalk from 'chalk';
|
||||
import boxen from 'boxen';
|
||||
import Table from 'cli-table3';
|
||||
import type { Task, TaskStatus, TaskPriority } from '@tm/core/types';
|
||||
import type { Task, TaskStatus, TaskPriority } from '@tm/core';
|
||||
|
||||
/**
|
||||
* Get colored status display with ASCII icons (matches scripts/modules/ui.js style)
|
||||
|
||||
@@ -181,7 +181,7 @@ Workflows upload artifacts that you can download:
|
||||
|
||||
- Check extension code compiles locally: `cd apps/extension && npm run build`
|
||||
- Verify tests pass locally: `npm run test`
|
||||
- Check for TypeScript errors: `npm run check-types`
|
||||
- Check for TypeScript errors: `npm run typecheck`
|
||||
|
||||
#### Packaging Failures
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ npm run build:css
|
||||
npm run build
|
||||
|
||||
# Type checking
|
||||
npm run check-types
|
||||
npm run typecheck
|
||||
|
||||
# Linting
|
||||
npm run lint
|
||||
|
||||
@@ -237,7 +237,7 @@
|
||||
"watch": "npm run watch:js & npm run watch:css",
|
||||
"watch:js": "node ./esbuild.js --watch",
|
||||
"watch:css": "npx @tailwindcss/cli -i ./src/webview/index.css -o ./dist/index.css --watch",
|
||||
"check-types": "tsc --noEmit"
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { createTaskMasterCore, type TaskMasterCore } from '@tm/core';
|
||||
import { createTmCore, type TmCore } from '@tm/core';
|
||||
import type { ExtensionLogger } from '../utils/logger';
|
||||
|
||||
export interface TerminalExecutionOptions {
|
||||
@@ -21,7 +21,7 @@ export interface TerminalExecutionResult {
|
||||
|
||||
export class TerminalManager {
|
||||
private terminals = new Map<string, vscode.Terminal>();
|
||||
private tmCore?: TaskMasterCore;
|
||||
private tmCore?: TmCore;
|
||||
|
||||
constructor(
|
||||
private context: vscode.ExtensionContext,
|
||||
@@ -49,7 +49,7 @@ export class TerminalManager {
|
||||
await this.initializeCore();
|
||||
|
||||
// Use tm-core to start the task (same as CLI)
|
||||
const startResult = await this.tmCore!.startTask(taskId, {
|
||||
const startResult = await this.tmCore!.tasks.start(taskId, {
|
||||
dryRun: false,
|
||||
force: false,
|
||||
updateStatus: true
|
||||
@@ -110,7 +110,7 @@ export class TerminalManager {
|
||||
if (!workspaceRoot) {
|
||||
throw new Error('No workspace folder found');
|
||||
}
|
||||
this.tmCore = await createTaskMasterCore({ projectPath: workspaceRoot });
|
||||
this.tmCore = await createTmCore({ projectPath: workspaceRoot });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,13 +144,9 @@ export class TerminalManager {
|
||||
});
|
||||
this.terminals.clear();
|
||||
|
||||
// Clear tm-core reference (no explicit cleanup needed)
|
||||
if (this.tmCore) {
|
||||
try {
|
||||
await this.tmCore.close();
|
||||
this.tmCore = undefined;
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to close tm-core:', error);
|
||||
}
|
||||
this.tmCore = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,5 +4,6 @@
|
||||
*/
|
||||
|
||||
export * from './tools/autopilot/index.js';
|
||||
export * from './tools/tasks/index.js';
|
||||
export * from './shared/utils.js';
|
||||
export * from './shared/types.js';
|
||||
|
||||
@@ -45,14 +45,27 @@ export async function handleApiResult<T>(options: {
|
||||
log?: any;
|
||||
errorPrefix?: string;
|
||||
projectRoot?: string;
|
||||
tag?: string; // Optional tag/brief to use instead of reading from state.json
|
||||
}): Promise<ContentResult> {
|
||||
const { result, log, errorPrefix = 'API error', projectRoot } = options;
|
||||
const {
|
||||
result,
|
||||
log,
|
||||
errorPrefix = 'API error',
|
||||
projectRoot,
|
||||
tag: providedTag
|
||||
} = options;
|
||||
|
||||
// Get version info for every response
|
||||
const versionInfo = getVersionInfo();
|
||||
|
||||
// Get current tag if project root is provided
|
||||
const currentTag = projectRoot ? getCurrentTag(projectRoot) : null;
|
||||
// Use provided tag if available, otherwise get from state.json
|
||||
// Note: For API storage, tm-core returns the brief name as the tag
|
||||
const currentTag =
|
||||
providedTag !== undefined
|
||||
? providedTag
|
||||
: projectRoot
|
||||
? getCurrentTag(projectRoot)
|
||||
: null;
|
||||
|
||||
if (!result.success) {
|
||||
const errorMsg = result.error?.message || `Unknown ${errorPrefix}`;
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
withNormalizedProjectRoot
|
||||
} from '../../shared/utils.js';
|
||||
import type { MCPContext } from '../../shared/types.js';
|
||||
import { createTaskMasterCore } from '@tm/core';
|
||||
import { createTmCore } from '@tm/core';
|
||||
import { WorkflowService } from '@tm/core';
|
||||
import type { FastMCP } from 'fastmcp';
|
||||
|
||||
@@ -83,17 +83,16 @@ export function registerAutopilotStartTool(server: FastMCP) {
|
||||
}
|
||||
|
||||
// Load task data and get current tag
|
||||
const core = await createTaskMasterCore({
|
||||
const core = await createTmCore({
|
||||
projectPath: projectRoot
|
||||
});
|
||||
|
||||
// Get current tag from ConfigManager
|
||||
const currentTag = core.getActiveTag();
|
||||
const currentTag = core.config.getActiveTag();
|
||||
|
||||
const taskResult = await core.getTaskWithSubtask(taskId);
|
||||
const taskResult = await core.tasks.get(taskId);
|
||||
|
||||
if (!taskResult || !taskResult.task) {
|
||||
await core.close();
|
||||
return handleApiResult({
|
||||
result: {
|
||||
success: false,
|
||||
@@ -108,7 +107,6 @@ export function registerAutopilotStartTool(server: FastMCP) {
|
||||
|
||||
// Validate task has subtasks
|
||||
if (!task.subtasks || task.subtasks.length === 0) {
|
||||
await core.close();
|
||||
return handleApiResult({
|
||||
result: {
|
||||
success: false,
|
||||
|
||||
136
apps/mcp/src/tools/tasks/get-task.tool.ts
Normal file
136
apps/mcp/src/tools/tasks/get-task.tool.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* @fileoverview get-task MCP tool
|
||||
* Get detailed information about a specific task by ID
|
||||
*/
|
||||
|
||||
// TEMPORARY: Using zod/v3 for Draft-07 JSON Schema compatibility with FastMCP's zod-to-json-schema
|
||||
// TODO: Revert to 'zod' when MCP spec issue is resolved (see PR #1323)
|
||||
import { z } from 'zod/v3';
|
||||
import {
|
||||
handleApiResult,
|
||||
withNormalizedProjectRoot
|
||||
} from '../../shared/utils.js';
|
||||
import type { MCPContext } from '../../shared/types.js';
|
||||
import { createTmCore, type Task } from '@tm/core';
|
||||
import type { FastMCP } from 'fastmcp';
|
||||
|
||||
const GetTaskSchema = z.object({
|
||||
id: z
|
||||
.string()
|
||||
.describe(
|
||||
'Task ID(s) to get (can be comma-separated for multiple tasks)'
|
||||
),
|
||||
status: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Filter subtasks by status (e.g., 'pending', 'done')"),
|
||||
projectRoot: z
|
||||
.string()
|
||||
.describe(
|
||||
'Absolute path to the project root directory (Optional, usually from session)'
|
||||
),
|
||||
tag: z.string().optional().describe('Tag context to operate on')
|
||||
});
|
||||
|
||||
type GetTaskArgs = z.infer<typeof GetTaskSchema>;
|
||||
|
||||
/**
|
||||
* Register the get_task tool with the MCP server
|
||||
*/
|
||||
export function registerGetTaskTool(server: FastMCP) {
|
||||
server.addTool({
|
||||
name: 'get_task',
|
||||
description: 'Get detailed information about a specific task',
|
||||
parameters: GetTaskSchema,
|
||||
execute: withNormalizedProjectRoot(
|
||||
async (args: GetTaskArgs, context: MCPContext) => {
|
||||
const { id, status, projectRoot, tag } = args;
|
||||
|
||||
try {
|
||||
context.log.info(
|
||||
`Getting task details for ID: ${id}${status ? ` (filtering subtasks by status: ${status})` : ''} in root: ${projectRoot}`
|
||||
);
|
||||
|
||||
// Create tm-core with logging callback
|
||||
const tmCore = await createTmCore({
|
||||
projectPath: projectRoot,
|
||||
loggerConfig: {
|
||||
mcpMode: true,
|
||||
logCallback: context.log
|
||||
}
|
||||
});
|
||||
|
||||
// Handle comma-separated IDs - parallelize for better performance
|
||||
const taskIds = id.split(',').map((tid) => tid.trim());
|
||||
const results = await Promise.all(
|
||||
taskIds.map((taskId) => tmCore.tasks.get(taskId, tag))
|
||||
);
|
||||
|
||||
const tasks: Task[] = [];
|
||||
for (const result of results) {
|
||||
if (!result.task) continue;
|
||||
|
||||
// If status filter is provided, filter subtasks (create copy to avoid mutation)
|
||||
if (status && result.task.subtasks) {
|
||||
const statusFilters = status
|
||||
.split(',')
|
||||
.map((s) => s.trim().toLowerCase());
|
||||
const filteredSubtasks = result.task.subtasks.filter((st) =>
|
||||
statusFilters.includes(String(st.status).toLowerCase())
|
||||
);
|
||||
tasks.push({ ...result.task, subtasks: filteredSubtasks });
|
||||
} else {
|
||||
tasks.push(result.task);
|
||||
}
|
||||
}
|
||||
|
||||
if (tasks.length === 0) {
|
||||
context.log.warn(`No tasks found for ID(s): ${id}`);
|
||||
return handleApiResult({
|
||||
result: {
|
||||
success: false,
|
||||
error: {
|
||||
message: `No tasks found for ID(s): ${id}`
|
||||
}
|
||||
},
|
||||
log: context.log,
|
||||
projectRoot
|
||||
});
|
||||
}
|
||||
|
||||
context.log.info(
|
||||
`Successfully retrieved ${tasks.length} task(s) for ID(s): ${id}`
|
||||
);
|
||||
|
||||
// Return single task if only one ID was requested, otherwise array
|
||||
const responseData = taskIds.length === 1 ? tasks[0] : tasks;
|
||||
|
||||
return handleApiResult({
|
||||
result: {
|
||||
success: true,
|
||||
data: responseData
|
||||
},
|
||||
log: context.log,
|
||||
projectRoot,
|
||||
tag
|
||||
});
|
||||
} catch (error: any) {
|
||||
context.log.error(`Error in get-task: ${error.message}`);
|
||||
if (error.stack) {
|
||||
context.log.debug(error.stack);
|
||||
}
|
||||
return handleApiResult({
|
||||
result: {
|
||||
success: false,
|
||||
error: {
|
||||
message: `Failed to get task: ${error.message}`
|
||||
}
|
||||
},
|
||||
log: context.log,
|
||||
projectRoot
|
||||
});
|
||||
}
|
||||
}
|
||||
)
|
||||
});
|
||||
}
|
||||
166
apps/mcp/src/tools/tasks/get-tasks.tool.ts
Normal file
166
apps/mcp/src/tools/tasks/get-tasks.tool.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
/**
|
||||
* @fileoverview get-tasks MCP tool
|
||||
* Get all tasks from Task Master with optional filtering
|
||||
*/
|
||||
|
||||
// TEMPORARY: Using zod/v3 for Draft-07 JSON Schema compatibility with FastMCP's zod-to-json-schema
|
||||
// TODO: Revert to 'zod' when MCP spec issue is resolved (see PR #1323)
|
||||
import { z } from 'zod/v3';
|
||||
import {
|
||||
handleApiResult,
|
||||
withNormalizedProjectRoot
|
||||
} from '../../shared/utils.js';
|
||||
import type { MCPContext } from '../../shared/types.js';
|
||||
import { createTmCore, type TaskStatus, type Task } from '@tm/core';
|
||||
import type { FastMCP } from 'fastmcp';
|
||||
|
||||
const GetTasksSchema = z.object({
|
||||
projectRoot: z
|
||||
.string()
|
||||
.describe('The directory of the project. Must be an absolute path.'),
|
||||
status: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"Filter tasks by status (e.g., 'pending', 'done') or multiple statuses separated by commas (e.g., 'blocked,deferred')"
|
||||
),
|
||||
withSubtasks: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe('Include subtasks nested within their parent tasks in the response'),
|
||||
tag: z.string().optional().describe('Tag context to operate on')
|
||||
});
|
||||
|
||||
type GetTasksArgs = z.infer<typeof GetTasksSchema>;
|
||||
|
||||
/**
|
||||
* Register the get_tasks tool with the MCP server
|
||||
*/
|
||||
export function registerGetTasksTool(server: FastMCP) {
|
||||
server.addTool({
|
||||
name: 'get_tasks',
|
||||
description:
|
||||
'Get all tasks from Task Master, optionally filtering by status and including subtasks.',
|
||||
parameters: GetTasksSchema,
|
||||
execute: withNormalizedProjectRoot(
|
||||
async (args: GetTasksArgs, context: MCPContext) => {
|
||||
const { projectRoot, status, withSubtasks, tag } = args;
|
||||
|
||||
try {
|
||||
context.log.info(
|
||||
`Getting tasks from ${projectRoot}${status ? ` with status filter: ${status}` : ''}${tag ? ` for tag: ${tag}` : ''}`
|
||||
);
|
||||
|
||||
// Create tm-core with logging callback
|
||||
const tmCore = await createTmCore({
|
||||
projectPath: projectRoot,
|
||||
loggerConfig: {
|
||||
mcpMode: true,
|
||||
logCallback: context.log
|
||||
}
|
||||
});
|
||||
|
||||
// Build filter
|
||||
const filter =
|
||||
status && status !== 'all'
|
||||
? {
|
||||
status: status
|
||||
.split(',')
|
||||
.map((s: string) => s.trim() as TaskStatus)
|
||||
}
|
||||
: undefined;
|
||||
|
||||
// Call tm-core tasks.list()
|
||||
const result = await tmCore.tasks.list({
|
||||
tag,
|
||||
filter,
|
||||
includeSubtasks: withSubtasks
|
||||
});
|
||||
|
||||
context.log.info(
|
||||
`Retrieved ${result.tasks?.length || 0} tasks (${result.filtered} filtered, ${result.total} total)`
|
||||
);
|
||||
|
||||
// Calculate stats using reduce for cleaner code
|
||||
const totalTasks = result.total;
|
||||
const taskCounts = result.tasks.reduce(
|
||||
(acc, task) => {
|
||||
acc[task.status] = (acc[task.status] || 0) + 1;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, number>
|
||||
);
|
||||
|
||||
const completionPercentage =
|
||||
totalTasks > 0 ? ((taskCounts.done || 0) / totalTasks) * 100 : 0;
|
||||
|
||||
// Count subtasks using reduce
|
||||
const subtaskCounts = result.tasks.reduce(
|
||||
(acc, task) => {
|
||||
task.subtasks?.forEach((st) => {
|
||||
acc.total++;
|
||||
acc[st.status] = (acc[st.status] || 0) + 1;
|
||||
});
|
||||
return acc;
|
||||
},
|
||||
{ total: 0 } as Record<string, number>
|
||||
);
|
||||
|
||||
const subtaskCompletionPercentage =
|
||||
subtaskCounts.total > 0
|
||||
? ((subtaskCounts.done || 0) / subtaskCounts.total) * 100
|
||||
: 0;
|
||||
|
||||
return handleApiResult({
|
||||
result: {
|
||||
success: true,
|
||||
data: {
|
||||
tasks: result.tasks as Task[],
|
||||
filter: status || 'all',
|
||||
stats: {
|
||||
total: totalTasks,
|
||||
completed: taskCounts.done || 0,
|
||||
inProgress: taskCounts['in-progress'] || 0,
|
||||
pending: taskCounts.pending || 0,
|
||||
blocked: taskCounts.blocked || 0,
|
||||
deferred: taskCounts.deferred || 0,
|
||||
cancelled: taskCounts.cancelled || 0,
|
||||
review: taskCounts.review || 0,
|
||||
completionPercentage,
|
||||
subtasks: {
|
||||
total: subtaskCounts.total,
|
||||
completed: subtaskCounts.done || 0,
|
||||
inProgress: subtaskCounts['in-progress'] || 0,
|
||||
pending: subtaskCounts.pending || 0,
|
||||
blocked: subtaskCounts.blocked || 0,
|
||||
deferred: subtaskCounts.deferred || 0,
|
||||
cancelled: subtaskCounts.cancelled || 0,
|
||||
completionPercentage: subtaskCompletionPercentage
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
log: context.log,
|
||||
projectRoot,
|
||||
tag: result.tag
|
||||
});
|
||||
} catch (error: any) {
|
||||
context.log.error(`Error in get-tasks: ${error.message}`);
|
||||
if (error.stack) {
|
||||
context.log.debug(error.stack);
|
||||
}
|
||||
return handleApiResult({
|
||||
result: {
|
||||
success: false,
|
||||
error: {
|
||||
message: `Failed to get tasks: ${error.message}`
|
||||
}
|
||||
},
|
||||
log: context.log,
|
||||
projectRoot
|
||||
});
|
||||
}
|
||||
}
|
||||
)
|
||||
});
|
||||
}
|
||||
7
apps/mcp/src/tools/tasks/index.ts
Normal file
7
apps/mcp/src/tools/tasks/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* @fileoverview Tasks MCP tools index
|
||||
* Exports all task-related tool registration functions
|
||||
*/
|
||||
|
||||
export { registerGetTasksTool } from './get-tasks.tool.js';
|
||||
export { registerGetTaskTool } from './get-task.tool.js';
|
||||
@@ -1,114 +0,0 @@
|
||||
/**
|
||||
* list-tasks.js
|
||||
* Direct function implementation for listing tasks
|
||||
*/
|
||||
|
||||
import { listTasks } from '../../../../scripts/modules/task-manager.js';
|
||||
import {
|
||||
enableSilentMode,
|
||||
disableSilentMode
|
||||
} from '../../../../scripts/modules/utils.js';
|
||||
|
||||
/**
|
||||
* Direct function wrapper for listTasks with error handling and caching.
|
||||
*
|
||||
* @param {Object} args - Command arguments (now expecting tasksJsonPath explicitly).
|
||||
* @param {string} args.tasksJsonPath - Path to the tasks.json file.
|
||||
* @param {string} args.reportPath - Path to the report file.
|
||||
* @param {string} args.status - Status of the task.
|
||||
* @param {boolean} args.withSubtasks - Whether to include subtasks.
|
||||
* @param {string} args.projectRoot - Project root path (for MCP/env fallback)
|
||||
* @param {string} args.tag - Tag for the task (optional)
|
||||
* @param {Object} log - Logger object.
|
||||
* @returns {Promise<Object>} - Task list result { success: boolean, data?: any, error?: { code: string, message: string } }.
|
||||
*/
|
||||
export async function listTasksDirect(args, log, context = {}) {
|
||||
// Destructure the explicit tasksJsonPath from args
|
||||
const { tasksJsonPath, reportPath, status, withSubtasks, projectRoot, tag } =
|
||||
args;
|
||||
const { session } = context;
|
||||
|
||||
if (!tasksJsonPath) {
|
||||
log.error('listTasksDirect called without tasksJsonPath');
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'MISSING_ARGUMENT',
|
||||
message: 'tasksJsonPath is required'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Use the explicit tasksJsonPath for cache key
|
||||
const statusFilter = status || 'all';
|
||||
const withSubtasksFilter = withSubtasks || false;
|
||||
|
||||
// Define the action function to be executed on cache miss
|
||||
const coreListTasksAction = async () => {
|
||||
try {
|
||||
// Enable silent mode to prevent console logs from interfering with JSON response
|
||||
enableSilentMode();
|
||||
|
||||
log.info(
|
||||
`Executing core listTasks function for path: ${tasksJsonPath}, filter: ${statusFilter}, subtasks: ${withSubtasksFilter}`
|
||||
);
|
||||
// Pass the explicit tasksJsonPath to the core function
|
||||
const resultData = listTasks(
|
||||
tasksJsonPath,
|
||||
statusFilter,
|
||||
reportPath,
|
||||
withSubtasksFilter,
|
||||
'json',
|
||||
{ projectRoot, session, tag }
|
||||
);
|
||||
|
||||
if (!resultData || !resultData.tasks) {
|
||||
log.error('Invalid or empty response from listTasks core function');
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'INVALID_CORE_RESPONSE',
|
||||
message: 'Invalid or empty response from listTasks core function'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
log.info(
|
||||
`Core listTasks function retrieved ${resultData.tasks.length} tasks`
|
||||
);
|
||||
|
||||
// Restore normal logging
|
||||
disableSilentMode();
|
||||
|
||||
return { success: true, data: resultData };
|
||||
} catch (error) {
|
||||
// Make sure to restore normal logging even if there's an error
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Core listTasks function failed: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'LIST_TASKS_CORE_ERROR',
|
||||
message: error.message || 'Failed to list tasks'
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await coreListTasksAction();
|
||||
log.info('listTasksDirect completed');
|
||||
return result;
|
||||
} catch (error) {
|
||||
log.error(`Unexpected error during listTasks: ${error.message}`);
|
||||
console.error(error.stack);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'UNEXPECTED_ERROR',
|
||||
message: error.message
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
/**
|
||||
* show-task.js
|
||||
* Direct function implementation for showing task details
|
||||
*/
|
||||
|
||||
import {
|
||||
findTaskById,
|
||||
readComplexityReport,
|
||||
readJSON
|
||||
} from '../../../../scripts/modules/utils.js';
|
||||
import { findTasksPath } from '../utils/path-utils.js';
|
||||
|
||||
/**
|
||||
* Direct function wrapper for getting task details.
|
||||
*
|
||||
* @param {Object} args - Command arguments.
|
||||
* @param {string} args.id - Task ID to show.
|
||||
* @param {string} [args.file] - Optional path to the tasks file (passed to findTasksPath).
|
||||
* @param {string} args.reportPath - Explicit path to the complexity report file.
|
||||
* @param {string} [args.status] - Optional status to filter subtasks by.
|
||||
* @param {string} args.projectRoot - Absolute path to the project root directory (already normalized by tool).
|
||||
* @param {string} [args.tag] - Tag for the task
|
||||
* @param {Object} log - Logger object.
|
||||
* @param {Object} context - Context object containing session data.
|
||||
* @returns {Promise<Object>} - Result object with success status and data/error information.
|
||||
*/
|
||||
export async function showTaskDirect(args, log) {
|
||||
// This function doesn't need session context since it only reads data
|
||||
// Destructure projectRoot and other args. projectRoot is assumed normalized.
|
||||
const { id, file, reportPath, status, projectRoot, tag } = args;
|
||||
|
||||
log.info(
|
||||
`Showing task direct function. ID: ${id}, File: ${file}, Status Filter: ${status}, ProjectRoot: ${projectRoot}`
|
||||
);
|
||||
|
||||
// --- Path Resolution using the passed (already normalized) projectRoot ---
|
||||
let tasksJsonPath;
|
||||
try {
|
||||
// Use the projectRoot passed directly from args
|
||||
tasksJsonPath = findTasksPath(
|
||||
{ projectRoot: projectRoot, file: file },
|
||||
log
|
||||
);
|
||||
log.info(`Resolved tasks path: ${tasksJsonPath}`);
|
||||
} catch (error) {
|
||||
log.error(`Error finding tasks.json: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'TASKS_FILE_NOT_FOUND',
|
||||
message: `Failed to find tasks.json: ${error.message}`
|
||||
}
|
||||
};
|
||||
}
|
||||
// --- End Path Resolution ---
|
||||
|
||||
// --- Rest of the function remains the same, using tasksJsonPath ---
|
||||
try {
|
||||
const tasksData = readJSON(tasksJsonPath, projectRoot, tag);
|
||||
if (!tasksData || !tasksData.tasks) {
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'INVALID_TASKS_DATA', message: 'Invalid tasks data' }
|
||||
};
|
||||
}
|
||||
|
||||
const complexityReport = readComplexityReport(reportPath);
|
||||
|
||||
// Parse comma-separated IDs
|
||||
const taskIds = id
|
||||
.split(',')
|
||||
.map((taskId) => taskId.trim())
|
||||
.filter((taskId) => taskId.length > 0);
|
||||
|
||||
if (taskIds.length === 0) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'INVALID_TASK_ID',
|
||||
message: 'No valid task IDs provided'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Handle single task ID (existing behavior)
|
||||
if (taskIds.length === 1) {
|
||||
const { task, originalSubtaskCount } = findTaskById(
|
||||
tasksData.tasks,
|
||||
taskIds[0],
|
||||
complexityReport,
|
||||
status
|
||||
);
|
||||
|
||||
if (!task) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'TASK_NOT_FOUND',
|
||||
message: `Task or subtask with ID ${taskIds[0]} not found`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
log.info(`Successfully retrieved task ${taskIds[0]}.`);
|
||||
|
||||
const returnData = { ...task };
|
||||
if (originalSubtaskCount !== null) {
|
||||
returnData._originalSubtaskCount = originalSubtaskCount;
|
||||
returnData._subtaskFilter = status;
|
||||
}
|
||||
|
||||
return { success: true, data: returnData };
|
||||
}
|
||||
|
||||
// Handle multiple task IDs
|
||||
const foundTasks = [];
|
||||
const notFoundIds = [];
|
||||
|
||||
taskIds.forEach((taskId) => {
|
||||
const { task, originalSubtaskCount } = findTaskById(
|
||||
tasksData.tasks,
|
||||
taskId,
|
||||
complexityReport,
|
||||
status
|
||||
);
|
||||
|
||||
if (task) {
|
||||
const taskData = { ...task };
|
||||
if (originalSubtaskCount !== null) {
|
||||
taskData._originalSubtaskCount = originalSubtaskCount;
|
||||
taskData._subtaskFilter = status;
|
||||
}
|
||||
foundTasks.push(taskData);
|
||||
} else {
|
||||
notFoundIds.push(taskId);
|
||||
}
|
||||
});
|
||||
|
||||
log.info(
|
||||
`Successfully retrieved ${foundTasks.length} of ${taskIds.length} requested tasks.`
|
||||
);
|
||||
|
||||
// Return multiple tasks with metadata
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
tasks: foundTasks,
|
||||
requestedIds: taskIds,
|
||||
foundCount: foundTasks.length,
|
||||
notFoundIds: notFoundIds,
|
||||
isMultiple: true
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
log.error(`Error showing task ${id}: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'TASK_OPERATION_ERROR',
|
||||
message: error.message
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -103,7 +103,7 @@ export async function updateSubtaskByIdDirect(args, log, context = {}) {
|
||||
}
|
||||
|
||||
try {
|
||||
// Execute core updateSubtaskById function
|
||||
// Call legacy script which handles both API and file storage via bridge
|
||||
const coreResult = await updateSubtaskById(
|
||||
tasksPath,
|
||||
subtaskIdStr,
|
||||
@@ -129,7 +129,7 @@ export async function updateSubtaskByIdDirect(args, log, context = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
// Subtask updated successfully
|
||||
const parentId = subtaskIdStr.split('.')[0];
|
||||
const successMessage = `Successfully updated subtask with ID ${subtaskIdStr}`;
|
||||
logWrapper.success(successMessage);
|
||||
return {
|
||||
@@ -137,7 +137,7 @@ export async function updateSubtaskByIdDirect(args, log, context = {}) {
|
||||
data: {
|
||||
message: `Successfully updated subtask with ID ${subtaskIdStr}`,
|
||||
subtaskId: subtaskIdStr,
|
||||
parentId: subtaskIdStr.split('.')[0],
|
||||
parentId: parentId,
|
||||
subtask: coreResult.updatedSubtask,
|
||||
tasksPath,
|
||||
useResearch,
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
isSilentMode
|
||||
} from '../../../../scripts/modules/utils.js';
|
||||
import { createLogWrapper } from '../../tools/utils.js';
|
||||
import { findTasksPath } from '../utils/path-utils.js';
|
||||
|
||||
/**
|
||||
* Direct function wrapper for updateTaskById with error handling.
|
||||
@@ -39,16 +40,6 @@ export async function updateTaskByIdDirect(args, log, context = {}) {
|
||||
`Updating task by ID via direct function. ID: ${id}, ProjectRoot: ${projectRoot}`
|
||||
);
|
||||
|
||||
// Check if tasksJsonPath was provided
|
||||
if (!tasksJsonPath) {
|
||||
const errorMessage = 'tasksJsonPath is required but was not provided.';
|
||||
logWrapper.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'MISSING_ARGUMENT', message: errorMessage }
|
||||
};
|
||||
}
|
||||
|
||||
// Check required parameters (id and prompt)
|
||||
if (!id) {
|
||||
const errorMessage =
|
||||
@@ -56,7 +47,7 @@ export async function updateTaskByIdDirect(args, log, context = {}) {
|
||||
logWrapper.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'MISSING_TASK_ID', message: errorMessage }
|
||||
error: { code: 'INPUT_VALIDATION_ERROR', message: errorMessage }
|
||||
};
|
||||
}
|
||||
|
||||
@@ -66,34 +57,39 @@ export async function updateTaskByIdDirect(args, log, context = {}) {
|
||||
logWrapper.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'MISSING_PROMPT', message: errorMessage }
|
||||
error: { code: 'INPUT_VALIDATION_ERROR', message: errorMessage }
|
||||
};
|
||||
}
|
||||
|
||||
// Parse taskId - handle both string and number values
|
||||
// Parse taskId - handle numeric, alphanumeric, and subtask IDs
|
||||
let taskId;
|
||||
if (typeof id === 'string') {
|
||||
// Handle subtask IDs (e.g., "5.2")
|
||||
if (id.includes('.')) {
|
||||
taskId = id; // Keep as string for subtask IDs
|
||||
} else {
|
||||
// Parse as integer for main task IDs
|
||||
taskId = parseInt(id, 10);
|
||||
if (Number.isNaN(taskId)) {
|
||||
const errorMessage = `Invalid task ID: ${id}. Task ID must be a positive integer or subtask ID (e.g., "5.2").`;
|
||||
logWrapper.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'INVALID_TASK_ID', message: errorMessage }
|
||||
};
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Keep ID as string - supports numeric (1, 2), alphanumeric (TAS-49, JIRA-123), and subtask IDs (1.2, TAS-49.1)
|
||||
taskId = id;
|
||||
} else if (typeof id === 'number') {
|
||||
// Convert number to string for consistency
|
||||
taskId = String(id);
|
||||
} else {
|
||||
const errorMessage = `Invalid task ID type: ${typeof id}. Task ID must be a string or number.`;
|
||||
logWrapper.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'INPUT_VALIDATION_ERROR', message: errorMessage }
|
||||
};
|
||||
}
|
||||
|
||||
// Use the provided path
|
||||
const tasksPath = tasksJsonPath;
|
||||
// Resolve tasks.json path - use provided or find it
|
||||
const tasksPath =
|
||||
tasksJsonPath ||
|
||||
findTasksPath({ projectRoot, file: args.file }, logWrapper);
|
||||
if (!tasksPath) {
|
||||
const errorMessage = 'tasks.json path could not be resolved.';
|
||||
logWrapper.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'INPUT_VALIDATION_ERROR', message: errorMessage }
|
||||
};
|
||||
}
|
||||
|
||||
// Get research flag
|
||||
const useResearch = research === true;
|
||||
@@ -108,7 +104,7 @@ export async function updateTaskByIdDirect(args, log, context = {}) {
|
||||
}
|
||||
|
||||
try {
|
||||
// Execute core updateTaskById function with proper parameters
|
||||
// Call legacy script which handles both API and file storage via bridge
|
||||
const coreResult = await updateTaskById(
|
||||
tasksPath,
|
||||
taskId,
|
||||
@@ -128,7 +124,6 @@ export async function updateTaskByIdDirect(args, log, context = {}) {
|
||||
|
||||
// Check if the core function returned null or an object without success
|
||||
if (!coreResult || coreResult.updatedTask === null) {
|
||||
// Core function logs the reason, just return success with info
|
||||
const message = `Task ${taskId} was not updated (likely already completed).`;
|
||||
logWrapper.info(message);
|
||||
return {
|
||||
@@ -143,9 +138,8 @@ export async function updateTaskByIdDirect(args, log, context = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
// Task was updated successfully
|
||||
const successMessage = `Successfully updated task with ID ${taskId} based on the prompt`;
|
||||
logWrapper.success(successMessage);
|
||||
logWrapper.info(successMessage);
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
*/
|
||||
|
||||
// Import direct function implementations
|
||||
import { listTasksDirect } from './direct-functions/list-tasks.js';
|
||||
import { getCacheStatsDirect } from './direct-functions/cache-stats.js';
|
||||
import { parsePRDDirect } from './direct-functions/parse-prd.js';
|
||||
import { updateTasksDirect } from './direct-functions/update-tasks.js';
|
||||
@@ -13,7 +12,6 @@ import { updateTaskByIdDirect } from './direct-functions/update-task-by-id.js';
|
||||
import { updateSubtaskByIdDirect } from './direct-functions/update-subtask-by-id.js';
|
||||
import { generateTaskFilesDirect } from './direct-functions/generate-task-files.js';
|
||||
import { setTaskStatusDirect } from './direct-functions/set-task-status.js';
|
||||
import { showTaskDirect } from './direct-functions/show-task.js';
|
||||
import { nextTaskDirect } from './direct-functions/next-task.js';
|
||||
import { expandTaskDirect } from './direct-functions/expand-task.js';
|
||||
import { addTaskDirect } from './direct-functions/add-task.js';
|
||||
@@ -47,7 +45,6 @@ export { findTasksPath } from './utils/path-utils.js';
|
||||
|
||||
// Use Map for potential future enhancements like introspection or dynamic dispatch
|
||||
export const directFunctions = new Map([
|
||||
['listTasksDirect', listTasksDirect],
|
||||
['getCacheStatsDirect', getCacheStatsDirect],
|
||||
['parsePRDDirect', parsePRDDirect],
|
||||
['updateTasksDirect', updateTasksDirect],
|
||||
@@ -55,7 +52,6 @@ export const directFunctions = new Map([
|
||||
['updateSubtaskByIdDirect', updateSubtaskByIdDirect],
|
||||
['generateTaskFilesDirect', generateTaskFilesDirect],
|
||||
['setTaskStatusDirect', setTaskStatusDirect],
|
||||
['showTaskDirect', showTaskDirect],
|
||||
['nextTaskDirect', nextTaskDirect],
|
||||
['expandTaskDirect', expandTaskDirect],
|
||||
['addTaskDirect', addTaskDirect],
|
||||
@@ -87,7 +83,6 @@ export const directFunctions = new Map([
|
||||
|
||||
// Re-export all direct function implementations
|
||||
export {
|
||||
listTasksDirect,
|
||||
getCacheStatsDirect,
|
||||
parsePRDDirect,
|
||||
updateTasksDirect,
|
||||
@@ -95,7 +90,6 @@ export {
|
||||
updateSubtaskByIdDirect,
|
||||
generateTaskFilesDirect,
|
||||
setTaskStatusDirect,
|
||||
showTaskDirect,
|
||||
nextTaskDirect,
|
||||
expandTaskDirect,
|
||||
addTaskDirect,
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
/**
|
||||
* tools/get-task.js
|
||||
* Tool to get task details by ID
|
||||
*/
|
||||
|
||||
// TEMPORARY: Using zod/v3 for Draft-07 JSON Schema compatibility with FastMCP's zod-to-json-schema
|
||||
// TODO: Revert to 'zod' when MCP spec issue is resolved (see PR #1323)
|
||||
import { z } from 'zod/v3';
|
||||
import {
|
||||
handleApiResult,
|
||||
createErrorResponse,
|
||||
withNormalizedProjectRoot
|
||||
} from './utils.js';
|
||||
import { showTaskDirect } from '../core/task-master-core.js';
|
||||
import {
|
||||
findTasksPath,
|
||||
findComplexityReportPath
|
||||
} from '../core/utils/path-utils.js';
|
||||
import { resolveTag } from '../../../scripts/modules/utils.js';
|
||||
|
||||
/**
|
||||
* Custom processor function that removes allTasks from the response
|
||||
* @param {Object} data - The data returned from showTaskDirect
|
||||
* @returns {Object} - The processed data with allTasks removed
|
||||
*/
|
||||
function processTaskResponse(data) {
|
||||
if (!data) return data;
|
||||
|
||||
// If we have the expected structure with task and allTasks
|
||||
if (typeof data === 'object' && data !== null && data.id && data.title) {
|
||||
// If the data itself looks like the task object, return it
|
||||
return data;
|
||||
} else if (data.task) {
|
||||
return data.task;
|
||||
}
|
||||
|
||||
// If structure is unexpected, return as is
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the get-task tool with the MCP server
|
||||
* @param {Object} server - FastMCP server instance
|
||||
*/
|
||||
export function registerShowTaskTool(server) {
|
||||
server.addTool({
|
||||
name: 'get_task',
|
||||
description: 'Get detailed information about a specific task',
|
||||
parameters: z.object({
|
||||
id: z
|
||||
.string()
|
||||
.describe(
|
||||
'Task ID(s) to get (can be comma-separated for multiple tasks)'
|
||||
),
|
||||
status: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Filter subtasks by status (e.g., 'pending', 'done')"),
|
||||
file: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Path to the tasks file relative to project root'),
|
||||
complexityReport: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
'Path to the complexity report file (relative to project root or absolute)'
|
||||
),
|
||||
projectRoot: z
|
||||
.string()
|
||||
.describe(
|
||||
'Absolute path to the project root directory (Optional, usually from session)'
|
||||
),
|
||||
tag: z.string().optional().describe('Tag context to operate on')
|
||||
}),
|
||||
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
|
||||
const { id, file, status, projectRoot } = args;
|
||||
|
||||
try {
|
||||
log.info(
|
||||
`Getting task details for ID: ${id}${status ? ` (filtering subtasks by status: ${status})` : ''} in root: ${projectRoot}`
|
||||
);
|
||||
const resolvedTag = resolveTag({
|
||||
projectRoot: args.projectRoot,
|
||||
tag: args.tag
|
||||
});
|
||||
|
||||
// Resolve the path to tasks.json using the NORMALIZED projectRoot from args
|
||||
let tasksJsonPath;
|
||||
try {
|
||||
tasksJsonPath = findTasksPath(
|
||||
{ projectRoot: projectRoot, file: file },
|
||||
log
|
||||
);
|
||||
log.info(`Resolved tasks path: ${tasksJsonPath}`);
|
||||
} catch (error) {
|
||||
log.error(`Error finding tasks.json: ${error.message}`);
|
||||
return createErrorResponse(
|
||||
`Failed to find tasks.json: ${error.message}`
|
||||
);
|
||||
}
|
||||
|
||||
// Call the direct function, passing the normalized projectRoot
|
||||
// Resolve the path to complexity report
|
||||
let complexityReportPath;
|
||||
try {
|
||||
complexityReportPath = findComplexityReportPath(
|
||||
{
|
||||
projectRoot: projectRoot,
|
||||
complexityReport: args.complexityReport,
|
||||
tag: resolvedTag
|
||||
},
|
||||
log
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(`Error finding complexity report: ${error.message}`);
|
||||
}
|
||||
const result = await showTaskDirect(
|
||||
{
|
||||
tasksJsonPath: tasksJsonPath,
|
||||
reportPath: complexityReportPath,
|
||||
// Pass other relevant args
|
||||
id: id,
|
||||
status: status,
|
||||
projectRoot: projectRoot,
|
||||
tag: resolvedTag
|
||||
},
|
||||
log,
|
||||
{ session }
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
log.info(`Successfully retrieved task details for ID: ${args.id}`);
|
||||
} else {
|
||||
log.error(`Failed to get task: ${result.error.message}`);
|
||||
}
|
||||
|
||||
// Use our custom processor function
|
||||
return handleApiResult(
|
||||
result,
|
||||
log,
|
||||
'Error retrieving task details',
|
||||
processTaskResponse,
|
||||
projectRoot
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(`Error in get-task tool: ${error.message}\n${error.stack}`);
|
||||
return createErrorResponse(`Failed to get task: ${error.message}`);
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
/**
|
||||
* tools/get-tasks.js
|
||||
* Tool to get all tasks from Task Master
|
||||
*/
|
||||
|
||||
// TEMPORARY: Using zod/v3 for Draft-07 JSON Schema compatibility with FastMCP's zod-to-json-schema
|
||||
// TODO: Revert to 'zod' when MCP spec issue is resolved (see PR #1323)
|
||||
import { z } from 'zod/v3';
|
||||
import {
|
||||
createErrorResponse,
|
||||
handleApiResult,
|
||||
withNormalizedProjectRoot
|
||||
} from './utils.js';
|
||||
import { listTasksDirect } from '../core/task-master-core.js';
|
||||
import {
|
||||
resolveTasksPath,
|
||||
resolveComplexityReportPath
|
||||
} from '../core/utils/path-utils.js';
|
||||
|
||||
import { resolveTag } from '../../../scripts/modules/utils.js';
|
||||
|
||||
/**
|
||||
* Register the getTasks tool with the MCP server
|
||||
* @param {Object} server - FastMCP server instance
|
||||
*/
|
||||
export function registerListTasksTool(server) {
|
||||
server.addTool({
|
||||
name: 'get_tasks',
|
||||
description:
|
||||
'Get all tasks from Task Master, optionally filtering by status and including subtasks.',
|
||||
parameters: z.object({
|
||||
status: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"Filter tasks by status (e.g., 'pending', 'done') or multiple statuses separated by commas (e.g., 'blocked,deferred')"
|
||||
),
|
||||
withSubtasks: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe(
|
||||
'Include subtasks nested within their parent tasks in the response'
|
||||
),
|
||||
file: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
'Path to the tasks file (relative to project root or absolute)'
|
||||
),
|
||||
complexityReport: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
'Path to the complexity report file (relative to project root or absolute)'
|
||||
),
|
||||
projectRoot: z
|
||||
.string()
|
||||
.describe('The directory of the project. Must be an absolute path.'),
|
||||
tag: z.string().optional().describe('Tag context to operate on')
|
||||
}),
|
||||
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
|
||||
try {
|
||||
log.info(`Getting tasks with filters: ${JSON.stringify(args)}`);
|
||||
|
||||
const resolvedTag = resolveTag({
|
||||
projectRoot: args.projectRoot,
|
||||
tag: args.tag
|
||||
});
|
||||
// Resolve the path to tasks.json using new path utilities
|
||||
let tasksJsonPath;
|
||||
try {
|
||||
tasksJsonPath = resolveTasksPath(args, log);
|
||||
} catch (error) {
|
||||
log.error(`Error finding tasks.json: ${error.message}`);
|
||||
return createErrorResponse(
|
||||
`Failed to find tasks.json: ${error.message}`
|
||||
);
|
||||
}
|
||||
|
||||
// Resolve the path to complexity report
|
||||
let complexityReportPath;
|
||||
try {
|
||||
complexityReportPath = resolveComplexityReportPath(
|
||||
{ ...args, tag: resolvedTag },
|
||||
session
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(`Error finding complexity report: ${error.message}`);
|
||||
// This is optional, so we don't fail the operation
|
||||
complexityReportPath = null;
|
||||
}
|
||||
|
||||
const result = await listTasksDirect(
|
||||
{
|
||||
tasksJsonPath: tasksJsonPath,
|
||||
status: args.status,
|
||||
withSubtasks: args.withSubtasks,
|
||||
reportPath: complexityReportPath,
|
||||
projectRoot: args.projectRoot,
|
||||
tag: resolvedTag
|
||||
},
|
||||
log,
|
||||
{ session }
|
||||
);
|
||||
|
||||
log.info(
|
||||
`Retrieved ${result.success ? result.data?.tasks?.length || 0 : 0} tasks`
|
||||
);
|
||||
return handleApiResult(
|
||||
result,
|
||||
log,
|
||||
'Error getting tasks',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(`Error getting tasks: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// We no longer need the formatTasksResponse function as we're returning raw JSON data
|
||||
@@ -1,16 +1,14 @@
|
||||
/**
|
||||
* tool-registry.js
|
||||
* Tool Registry Object Structure - Maps all 36 tool names to registration functions
|
||||
* Tool Registry - Maps tool names to registration functions
|
||||
*/
|
||||
|
||||
import { registerListTasksTool } from './get-tasks.js';
|
||||
import { registerSetTaskStatusTool } from './set-task-status.js';
|
||||
import { registerParsePRDTool } from './parse-prd.js';
|
||||
import { registerUpdateTool } from './update.js';
|
||||
import { registerUpdateTaskTool } from './update-task.js';
|
||||
import { registerUpdateSubtaskTool } from './update-subtask.js';
|
||||
import { registerGenerateTool } from './generate.js';
|
||||
import { registerShowTaskTool } from './get-task.js';
|
||||
import { registerNextTaskTool } from './next-task.js';
|
||||
import { registerExpandTaskTool } from './expand-task.js';
|
||||
import { registerAddTaskTool } from './add-task.js';
|
||||
@@ -40,7 +38,7 @@ import { registerRulesTool } from './rules.js';
|
||||
import { registerScopeUpTool } from './scope-up.js';
|
||||
import { registerScopeDownTool } from './scope-down.js';
|
||||
|
||||
// Import TypeScript autopilot tools from apps/mcp
|
||||
// Import TypeScript tools from apps/mcp
|
||||
import {
|
||||
registerAutopilotStartTool,
|
||||
registerAutopilotResumeTool,
|
||||
@@ -49,7 +47,9 @@ import {
|
||||
registerAutopilotCompleteTool,
|
||||
registerAutopilotCommitTool,
|
||||
registerAutopilotFinalizeTool,
|
||||
registerAutopilotAbortTool
|
||||
registerAutopilotAbortTool,
|
||||
registerGetTasksTool,
|
||||
registerGetTaskTool
|
||||
} from '@tm/mcp';
|
||||
|
||||
/**
|
||||
@@ -67,8 +67,8 @@ export const toolRegistry = {
|
||||
expand_all: registerExpandAllTool,
|
||||
scope_up_task: registerScopeUpTool,
|
||||
scope_down_task: registerScopeDownTool,
|
||||
get_tasks: registerListTasksTool,
|
||||
get_task: registerShowTaskTool,
|
||||
get_tasks: registerGetTasksTool,
|
||||
get_task: registerGetTaskTool,
|
||||
next_task: registerNextTaskTool,
|
||||
complexity_report: registerComplexityReportTool,
|
||||
set_task_status: registerSetTaskStatusTool,
|
||||
|
||||
285
package-lock.json
generated
285
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "task-master-ai",
|
||||
"version": "0.30.0",
|
||||
"version": "0.30.1-rc.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "task-master-ai",
|
||||
"version": "0.30.0",
|
||||
"version": "0.30.1-rc.0",
|
||||
"license": "MIT WITH Commons-Clause",
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
@@ -79,6 +79,7 @@
|
||||
"@manypkg/cli": "^0.25.1",
|
||||
"@tm/ai-sdk-provider-grok-cli": "*",
|
||||
"@tm/cli": "*",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/marked-terminal": "^6.1.1",
|
||||
"concurrently": "^9.2.1",
|
||||
@@ -9359,6 +9360,10 @@
|
||||
"resolved": "packages/ai-sdk-provider-grok-cli",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@tm/bridge": {
|
||||
"resolved": "packages/tm-bridge",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@tm/build-config": {
|
||||
"resolved": "packages/build-config",
|
||||
"link": true
|
||||
@@ -28213,6 +28218,282 @@
|
||||
"version": "0.0.2",
|
||||
"license": "MIT WITH Commons-Clause"
|
||||
},
|
||||
"packages/tm-bridge": {
|
||||
"name": "@tm/bridge",
|
||||
"dependencies": {
|
||||
"@tm/core": "*",
|
||||
"boxen": "^8.0.1",
|
||||
"chalk": "5.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.5",
|
||||
"typescript": "^5.9.2",
|
||||
"vitest": "^3.2.4"
|
||||
}
|
||||
},
|
||||
"packages/tm-bridge/node_modules/@vitest/expect": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz",
|
||||
"integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/spy": "3.2.4",
|
||||
"@vitest/utils": "3.2.4",
|
||||
"chai": "^5.2.0",
|
||||
"tinyrainbow": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"packages/tm-bridge/node_modules/@vitest/pretty-format": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz",
|
||||
"integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tinyrainbow": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"packages/tm-bridge/node_modules/@vitest/runner": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz",
|
||||
"integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/utils": "3.2.4",
|
||||
"pathe": "^2.0.3",
|
||||
"strip-literal": "^3.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"packages/tm-bridge/node_modules/@vitest/snapshot": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz",
|
||||
"integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "3.2.4",
|
||||
"magic-string": "^0.30.17",
|
||||
"pathe": "^2.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"packages/tm-bridge/node_modules/@vitest/spy": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz",
|
||||
"integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tinyspy": "^4.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"packages/tm-bridge/node_modules/@vitest/utils": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz",
|
||||
"integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "3.2.4",
|
||||
"loupe": "^3.1.4",
|
||||
"tinyrainbow": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"packages/tm-bridge/node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"packages/tm-bridge/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"packages/tm-bridge/node_modules/tinyexec": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
|
||||
"integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"packages/tm-bridge/node_modules/tinyrainbow": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz",
|
||||
"integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"packages/tm-bridge/node_modules/tinyspy": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz",
|
||||
"integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"packages/tm-bridge/node_modules/vite-node": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz",
|
||||
"integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cac": "^6.7.14",
|
||||
"debug": "^4.4.1",
|
||||
"es-module-lexer": "^1.7.0",
|
||||
"pathe": "^2.0.3",
|
||||
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
|
||||
},
|
||||
"bin": {
|
||||
"vite-node": "vite-node.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"packages/tm-bridge/node_modules/vitest": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
|
||||
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/expect": "3.2.4",
|
||||
"@vitest/mocker": "3.2.4",
|
||||
"@vitest/pretty-format": "^3.2.4",
|
||||
"@vitest/runner": "3.2.4",
|
||||
"@vitest/snapshot": "3.2.4",
|
||||
"@vitest/spy": "3.2.4",
|
||||
"@vitest/utils": "3.2.4",
|
||||
"chai": "^5.2.0",
|
||||
"debug": "^4.4.1",
|
||||
"expect-type": "^1.2.1",
|
||||
"magic-string": "^0.30.17",
|
||||
"pathe": "^2.0.3",
|
||||
"picomatch": "^4.0.2",
|
||||
"std-env": "^3.9.0",
|
||||
"tinybench": "^2.9.0",
|
||||
"tinyexec": "^0.3.2",
|
||||
"tinyglobby": "^0.2.14",
|
||||
"tinypool": "^1.1.1",
|
||||
"tinyrainbow": "^2.0.0",
|
||||
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0",
|
||||
"vite-node": "3.2.4",
|
||||
"why-is-node-running": "^2.3.0"
|
||||
},
|
||||
"bin": {
|
||||
"vitest": "vitest.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@edge-runtime/vm": "*",
|
||||
"@types/debug": "^4.1.12",
|
||||
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
|
||||
"@vitest/browser": "3.2.4",
|
||||
"@vitest/ui": "3.2.4",
|
||||
"happy-dom": "*",
|
||||
"jsdom": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@edge-runtime/vm": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/debug": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/browser": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/ui": {
|
||||
"optional": true
|
||||
},
|
||||
"happy-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"jsdom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"packages/tm-bridge/node_modules/vitest/node_modules/@vitest/mocker": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz",
|
||||
"integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/spy": "3.2.4",
|
||||
"estree-walker": "^3.0.3",
|
||||
"magic-string": "^0.30.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"msw": "^2.4.9",
|
||||
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"msw": {
|
||||
"optional": true
|
||||
},
|
||||
"vite": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"packages/tm-core": {
|
||||
"name": "@tm/core",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "task-master-ai",
|
||||
"version": "0.30.0",
|
||||
"version": "0.30.1-rc.0",
|
||||
"description": "A task management system for ambitious AI-driven development that doesn't overwhelm and confuse Cursor.",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
@@ -139,6 +139,7 @@
|
||||
"@manypkg/cli": "^0.25.1",
|
||||
"@tm/ai-sdk-provider-grok-cli": "*",
|
||||
"@tm/cli": "*",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/marked-terminal": "^6.1.1",
|
||||
"concurrently": "^9.2.1",
|
||||
|
||||
53
packages/tm-bridge/README.md
Normal file
53
packages/tm-bridge/README.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# @tm/bridge
|
||||
|
||||
> ⚠️ **TEMPORARY PACKAGE - DELETE WHEN LEGACY CODE IS REMOVED** ⚠️
|
||||
|
||||
This package exists solely as a bridge between legacy scripts (`scripts/modules/task-manager/`) and the new tm-core architecture. It provides shared functionality that both CLI and MCP direct functions can use during the migration period.
|
||||
|
||||
## Why does this exist?
|
||||
|
||||
During the transition from legacy scripts to tm-core, we need a single source of truth for bridge logic that handles:
|
||||
- API vs file storage detection
|
||||
- Remote AI service delegation
|
||||
- Consistent behavior across CLI and MCP interfaces
|
||||
|
||||
## When to delete this
|
||||
|
||||
Delete this entire package when:
|
||||
1. ✅ Legacy scripts in `scripts/modules/task-manager/` are removed
|
||||
2. ✅ MCP direct functions in `mcp-server/src/core/direct-functions/` are removed
|
||||
3. ✅ All functionality has moved to tm-core
|
||||
4. ✅ CLI and MCP use tm-core directly via TasksDomain
|
||||
|
||||
## Migration path
|
||||
|
||||
```text
|
||||
Current: CLI → legacy scripts → @tm/bridge → @tm/core
|
||||
MCP → direct functions → legacy scripts → @tm/bridge → @tm/core
|
||||
|
||||
Future: CLI → @tm/core (TasksDomain)
|
||||
MCP → @tm/core (TasksDomain)
|
||||
|
||||
DELETE: legacy scripts, direct functions, @tm/bridge
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```typescript
|
||||
import { tryUpdateViaRemote } from '@tm/bridge';
|
||||
|
||||
const result = await tryUpdateViaRemote({
|
||||
taskId: '1.2',
|
||||
prompt: 'Update task...',
|
||||
projectRoot: '/path/to/project',
|
||||
// ... other params
|
||||
});
|
||||
```
|
||||
|
||||
## Contents
|
||||
|
||||
- `update-bridge.ts` - Shared update bridge logic for task/subtask updates
|
||||
|
||||
---
|
||||
|
||||
**Remember:** This package should NOT accumulate new features. It's a temporary migration aid only.
|
||||
31
packages/tm-bridge/package.json
Normal file
31
packages/tm-bridge/package.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "@tm/bridge",
|
||||
"private": true,
|
||||
"description": "TEMPORARY: Bridge layer for legacy code migration. DELETE when legacy scripts are removed.",
|
||||
"type": "module",
|
||||
"types": "./src/index.ts",
|
||||
"main": "./dist/index.js",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"lint": "biome check --write",
|
||||
"lint:check": "biome check",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tm/core": "*",
|
||||
"chalk": "5.6.2",
|
||||
"boxen": "^8.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.5",
|
||||
"typescript": "^5.9.2",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"files": ["src", "README.md"],
|
||||
"keywords": ["temporary", "bridge", "migration"],
|
||||
"author": "Task Master AI"
|
||||
}
|
||||
16
packages/tm-bridge/src/index.ts
Normal file
16
packages/tm-bridge/src/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* @tm/bridge - Temporary bridge package for legacy code migration
|
||||
*
|
||||
* ⚠️ THIS PACKAGE IS TEMPORARY AND WILL BE DELETED ⚠️
|
||||
*
|
||||
* This package exists solely to provide shared bridge logic between
|
||||
* legacy scripts and the new tm-core architecture during migration.
|
||||
*
|
||||
* DELETE THIS PACKAGE when legacy scripts are removed.
|
||||
*/
|
||||
|
||||
export {
|
||||
tryUpdateViaRemote,
|
||||
type UpdateBridgeParams,
|
||||
type RemoteUpdateResult
|
||||
} from './update-bridge.js';
|
||||
183
packages/tm-bridge/src/update-bridge.ts
Normal file
183
packages/tm-bridge/src/update-bridge.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
import chalk from 'chalk';
|
||||
import boxen from 'boxen';
|
||||
import { createTmCore, type TmCore } from '@tm/core';
|
||||
|
||||
/**
|
||||
* Parameters for the update bridge function
|
||||
*/
|
||||
export interface UpdateBridgeParams {
|
||||
/** Task ID (can be numeric "1", alphanumeric "TAS-49", or dotted "1.2" or "TAS-49.1") */
|
||||
taskId: string | number;
|
||||
/** Update prompt for AI */
|
||||
prompt: string;
|
||||
/** Project root directory */
|
||||
projectRoot: string;
|
||||
/** Optional tag for task organization */
|
||||
tag?: string;
|
||||
/** Whether to append or full update (default: false) */
|
||||
appendMode?: boolean;
|
||||
/** Whether called from MCP context (default: false) */
|
||||
isMCP?: boolean;
|
||||
/** Output format (default: 'text') */
|
||||
outputFormat?: 'text' | 'json';
|
||||
/** Logging function */
|
||||
report: (level: string, ...args: unknown[]) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result returned when API storage handles the update
|
||||
*/
|
||||
export interface RemoteUpdateResult {
|
||||
success: boolean;
|
||||
taskId: string | number;
|
||||
message: string;
|
||||
telemetryData: null;
|
||||
tagInfo: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared bridge function for update-task and update-subtask commands.
|
||||
* Checks if using API storage and delegates to remote AI service if so.
|
||||
*
|
||||
* In API storage, tasks and subtasks are treated identically - there's no
|
||||
* parent/child hierarchy, so update-task and update-subtask can be used
|
||||
* interchangeably.
|
||||
*
|
||||
* @param params - Bridge parameters
|
||||
* @returns Result object if API storage handled it, null if should fall through to file storage
|
||||
*/
|
||||
export async function tryUpdateViaRemote(
|
||||
params: UpdateBridgeParams
|
||||
): Promise<RemoteUpdateResult | null> {
|
||||
const {
|
||||
taskId,
|
||||
prompt,
|
||||
projectRoot,
|
||||
tag,
|
||||
appendMode = false,
|
||||
isMCP = false,
|
||||
outputFormat = 'text',
|
||||
report
|
||||
} = params;
|
||||
|
||||
let tmCore: TmCore;
|
||||
|
||||
try {
|
||||
tmCore = await createTmCore({
|
||||
projectPath: projectRoot || process.cwd()
|
||||
});
|
||||
} catch (tmCoreError) {
|
||||
const errorMessage =
|
||||
tmCoreError instanceof Error ? tmCoreError.message : String(tmCoreError);
|
||||
report(
|
||||
'warn',
|
||||
`TmCore check failed, falling back to file-based update: ${errorMessage}`
|
||||
);
|
||||
// Return null to signal fall-through to file storage logic
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if we're using API storage (use resolved storage type, not config)
|
||||
const storageType = tmCore.tasks.getStorageType();
|
||||
|
||||
if (storageType !== 'api') {
|
||||
// Not API storage - signal caller to fall through to file-based logic
|
||||
report(
|
||||
'info',
|
||||
`Using file storage - processing update locally for task ${taskId}`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
// API STORAGE PATH: Delegate to remote AI service
|
||||
report('info', `Delegating update to Hamster for task ${taskId}`);
|
||||
|
||||
const mode = appendMode ? 'append' : 'update';
|
||||
|
||||
// Show CLI output if not MCP
|
||||
if (!isMCP && outputFormat === 'text') {
|
||||
const showDebug = process.env.TM_DEBUG === '1';
|
||||
const promptPreview = showDebug
|
||||
? `${prompt.substring(0, 60)}${prompt.length > 60 ? '...' : ''}`
|
||||
: '[hidden]';
|
||||
|
||||
console.log(
|
||||
boxen(
|
||||
chalk.blue.bold(`Updating Task via Hamster`) +
|
||||
'\n\n' +
|
||||
chalk.white(`Task ID: ${taskId}`) +
|
||||
'\n' +
|
||||
chalk.white(`Mode: ${mode}`) +
|
||||
'\n' +
|
||||
chalk.white(`Prompt: ${promptPreview}`),
|
||||
{
|
||||
padding: 1,
|
||||
borderColor: 'blue',
|
||||
borderStyle: 'round',
|
||||
margin: { top: 1, bottom: 1 }
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
let loadingIndicator: NodeJS.Timeout | null = null;
|
||||
if (!isMCP && outputFormat === 'text') {
|
||||
// Simple loading indicator simulation (replace with actual startLoadingIndicator if available)
|
||||
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
||||
let frameIndex = 0;
|
||||
loadingIndicator = setInterval(() => {
|
||||
process.stdout.write(
|
||||
`\r${frames[frameIndex]} Updating task on Hamster...`
|
||||
);
|
||||
frameIndex = (frameIndex + 1) % frames.length;
|
||||
}, 80);
|
||||
}
|
||||
|
||||
try {
|
||||
// Call the API storage method which handles the remote update
|
||||
await tmCore.tasks.updateWithPrompt(String(taskId), prompt, tag, {
|
||||
mode
|
||||
});
|
||||
|
||||
if (loadingIndicator) {
|
||||
clearInterval(loadingIndicator);
|
||||
process.stdout.write('\r✓ Task updated successfully.\n');
|
||||
}
|
||||
|
||||
if (outputFormat === 'text') {
|
||||
console.log(
|
||||
boxen(
|
||||
chalk.green(`Successfully updated task ${taskId} via remote AI`) +
|
||||
'\n\n' +
|
||||
chalk.white('The task has been updated on the remote server.') +
|
||||
'\n' +
|
||||
chalk.white(
|
||||
`Run ${chalk.yellow(`task-master show ${taskId}`)} to view the updated task.`
|
||||
),
|
||||
{
|
||||
padding: 1,
|
||||
borderColor: 'green',
|
||||
borderStyle: 'round'
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Return success result - signals that we handled it
|
||||
return {
|
||||
success: true,
|
||||
taskId: taskId,
|
||||
message: 'Task updated via remote AI service',
|
||||
telemetryData: null,
|
||||
tagInfo: null
|
||||
};
|
||||
} catch (updateError) {
|
||||
if (loadingIndicator) {
|
||||
clearInterval(loadingIndicator);
|
||||
process.stdout.write('\r✗ Update failed.\n');
|
||||
}
|
||||
|
||||
// tm-core already formatted the error properly, just re-throw
|
||||
throw updateError;
|
||||
}
|
||||
}
|
||||
37
packages/tm-bridge/tsconfig.json
Normal file
37
packages/tm-bridge/tsconfig.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"lib": ["ES2022"],
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"baseUrl": ".",
|
||||
"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": "NodeNext",
|
||||
"moduleDetection": "force",
|
||||
"types": ["node"],
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"allowImportingTsExtensions": false
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "tests", "**/*.test.ts", "**/*.spec.ts"]
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
/**
|
||||
* Authentication module exports
|
||||
*/
|
||||
|
||||
export { AuthManager } from './auth-manager.js';
|
||||
export { CredentialStore } from './credential-store.js';
|
||||
export { OAuthService } from './oauth-service.js';
|
||||
export { SupabaseSessionStorage } from './supabase-session-storage.js';
|
||||
export type {
|
||||
Organization,
|
||||
Brief,
|
||||
RemoteTask
|
||||
} from '../services/organization.service.js';
|
||||
|
||||
export type {
|
||||
AuthCredentials,
|
||||
OAuthFlowOptions,
|
||||
AuthConfig,
|
||||
CliData,
|
||||
UserContext
|
||||
} from './types.js';
|
||||
|
||||
export { AuthenticationError } from './types.js';
|
||||
|
||||
export {
|
||||
DEFAULT_AUTH_CONFIG,
|
||||
getAuthConfig
|
||||
} from './config.js';
|
||||
@@ -8,8 +8,8 @@ export type * from './storage.interface.js';
|
||||
export * from './storage.interface.js';
|
||||
|
||||
// AI Provider interfaces
|
||||
export type * from './ai-provider.interface.js';
|
||||
export * from './ai-provider.interface.js';
|
||||
export type * from '../../modules/ai/interfaces/ai-provider.interface.js';
|
||||
export * from '../../modules/ai/interfaces/ai-provider.interface.js';
|
||||
|
||||
// Configuration interfaces
|
||||
export type * from './configuration.interface.js';
|
||||
@@ -63,7 +63,7 @@ export interface IStorage {
|
||||
appendTasks(tasks: Task[], tag?: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Update a specific task by ID
|
||||
* Update a specific task by ID (direct structural update)
|
||||
* @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
|
||||
@@ -75,6 +75,23 @@ export interface IStorage {
|
||||
tag?: string
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* Update a task using AI-powered prompt (natural language update)
|
||||
* @param taskId - ID of the task to update
|
||||
* @param prompt - Natural language prompt describing the update
|
||||
* @param tag - Optional tag context for the task
|
||||
* @param options - Optional update options
|
||||
* @param options.useResearch - Whether to use research capabilities (for file storage)
|
||||
* @param options.mode - Update mode: 'append' adds info, 'update' makes targeted changes, 'rewrite' restructures (for API storage)
|
||||
* @returns Promise that resolves when update is complete
|
||||
*/
|
||||
updateTaskWithPrompt(
|
||||
taskId: string,
|
||||
prompt: string,
|
||||
tag?: string,
|
||||
options?: { useResearch?: boolean; mode?: 'append' | 'update' | 'rewrite' }
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* Update task or subtask status by ID
|
||||
* @param taskId - ID of the task or subtask (e.g., "1" or "1.2")
|
||||
@@ -164,6 +181,18 @@ export interface IStorage {
|
||||
* @returns Promise that resolves to storage statistics
|
||||
*/
|
||||
getStats(): Promise<StorageStats>;
|
||||
|
||||
/**
|
||||
* Get the storage type identifier
|
||||
* @returns The type of storage implementation ('file' or 'api')
|
||||
*/
|
||||
getStorageType(): 'file' | 'api';
|
||||
|
||||
/**
|
||||
* Get the current brief name (only applicable for API storage)
|
||||
* @returns The brief name if using API storage with a selected brief, null otherwise
|
||||
*/
|
||||
getCurrentBriefName(): string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -225,6 +254,12 @@ export abstract class BaseStorage implements IStorage {
|
||||
updates: Partial<Task>,
|
||||
tag?: string
|
||||
): Promise<void>;
|
||||
abstract updateTaskWithPrompt(
|
||||
taskId: string,
|
||||
prompt: string,
|
||||
tag?: string,
|
||||
options?: { useResearch?: boolean; mode?: 'append' | 'update' | 'rewrite' }
|
||||
): Promise<void>;
|
||||
abstract updateTaskStatus(
|
||||
taskId: string,
|
||||
newStatus: TaskStatus,
|
||||
@@ -241,7 +276,8 @@ export abstract class BaseStorage implements IStorage {
|
||||
abstract initialize(): Promise<void>;
|
||||
abstract close(): Promise<void>;
|
||||
abstract getStats(): Promise<StorageStats>;
|
||||
|
||||
abstract getStorageType(): 'file' | 'api';
|
||||
abstract getCurrentBriefName(): string | null;
|
||||
/**
|
||||
* Utility method to generate backup filename
|
||||
* @param originalPath - Original file path
|
||||
@@ -4,5 +4,5 @@
|
||||
*/
|
||||
|
||||
export { Logger, LogLevel } from './logger.js';
|
||||
export type { LoggerConfig } from './logger.js';
|
||||
export type { LoggerConfig, LogCallback, LogObject } from './logger.js';
|
||||
export { createLogger, getLogger, setGlobalLogger } from './factory.js';
|
||||
389
packages/tm-core/src/common/logger/logger.spec.ts
Normal file
389
packages/tm-core/src/common/logger/logger.spec.ts
Normal file
@@ -0,0 +1,389 @@
|
||||
/**
|
||||
* @fileoverview Tests for MCP logging integration
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { Logger, LogLevel, type LogCallback } from './logger.js';
|
||||
|
||||
describe('Logger - MCP Integration', () => {
|
||||
// Store original environment
|
||||
let originalEnv: Record<string, string | undefined>;
|
||||
|
||||
beforeEach(() => {
|
||||
// Save original environment
|
||||
originalEnv = {
|
||||
MCP_MODE: process.env.MCP_MODE,
|
||||
TASK_MASTER_MCP: process.env.TASK_MASTER_MCP,
|
||||
TASK_MASTER_SILENT: process.env.TASK_MASTER_SILENT,
|
||||
TM_SILENT: process.env.TM_SILENT,
|
||||
TASK_MASTER_LOG_LEVEL: process.env.TASK_MASTER_LOG_LEVEL,
|
||||
TM_LOG_LEVEL: process.env.TM_LOG_LEVEL,
|
||||
NO_COLOR: process.env.NO_COLOR,
|
||||
TASK_MASTER_NO_COLOR: process.env.TASK_MASTER_NO_COLOR
|
||||
};
|
||||
|
||||
// Clear environment variables for clean tests
|
||||
delete process.env.MCP_MODE;
|
||||
delete process.env.TASK_MASTER_MCP;
|
||||
delete process.env.TASK_MASTER_SILENT;
|
||||
delete process.env.TM_SILENT;
|
||||
delete process.env.TASK_MASTER_LOG_LEVEL;
|
||||
delete process.env.TM_LOG_LEVEL;
|
||||
delete process.env.NO_COLOR;
|
||||
delete process.env.TASK_MASTER_NO_COLOR;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Restore original environment
|
||||
for (const [key, value] of Object.entries(originalEnv)) {
|
||||
if (value === undefined) {
|
||||
delete process.env[key];
|
||||
} else {
|
||||
process.env[key] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
describe('Callback-based logging', () => {
|
||||
it('should call callback instead of console when logCallback is provided', () => {
|
||||
const mockCallback = vi.fn();
|
||||
const logger = new Logger({
|
||||
level: LogLevel.INFO,
|
||||
logCallback: mockCallback
|
||||
});
|
||||
|
||||
logger.info('Test message');
|
||||
|
||||
expect(mockCallback).toHaveBeenCalledWith(
|
||||
'info',
|
||||
expect.stringContaining('Test message')
|
||||
);
|
||||
});
|
||||
|
||||
it('should call callback for all log levels', () => {
|
||||
const mockCallback = vi.fn();
|
||||
const logger = new Logger({
|
||||
level: LogLevel.DEBUG,
|
||||
logCallback: mockCallback
|
||||
});
|
||||
|
||||
logger.error('Error message');
|
||||
logger.warn('Warning message');
|
||||
logger.info('Info message');
|
||||
logger.debug('Debug message');
|
||||
|
||||
expect(mockCallback).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'error',
|
||||
expect.stringContaining('Error message')
|
||||
);
|
||||
expect(mockCallback).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'warn',
|
||||
expect.stringContaining('Warning message')
|
||||
);
|
||||
expect(mockCallback).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
'info',
|
||||
expect.stringContaining('Info message')
|
||||
);
|
||||
expect(mockCallback).toHaveBeenNthCalledWith(
|
||||
4,
|
||||
'debug',
|
||||
expect.stringContaining('Debug message')
|
||||
);
|
||||
});
|
||||
|
||||
it('should respect log level with callback', () => {
|
||||
const mockCallback = vi.fn();
|
||||
const logger = new Logger({
|
||||
level: LogLevel.WARN,
|
||||
logCallback: mockCallback
|
||||
});
|
||||
|
||||
logger.debug('Debug message');
|
||||
logger.info('Info message');
|
||||
logger.warn('Warning message');
|
||||
logger.error('Error message');
|
||||
|
||||
// Only warn and error should be logged
|
||||
expect(mockCallback).toHaveBeenCalledTimes(2);
|
||||
expect(mockCallback).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'warn',
|
||||
expect.stringContaining('Warning message')
|
||||
);
|
||||
expect(mockCallback).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'error',
|
||||
expect.stringContaining('Error message')
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle raw log() calls with callback', () => {
|
||||
const mockCallback = vi.fn();
|
||||
const logger = new Logger({
|
||||
level: LogLevel.INFO,
|
||||
logCallback: mockCallback
|
||||
});
|
||||
|
||||
logger.log('Raw message', 'with args');
|
||||
|
||||
expect(mockCallback).toHaveBeenCalledWith('log', 'Raw message with args');
|
||||
});
|
||||
});
|
||||
|
||||
describe('MCP mode with callback', () => {
|
||||
it('should not silence logs when mcpMode=true and callback is provided', () => {
|
||||
const mockCallback = vi.fn();
|
||||
const logger = new Logger({
|
||||
level: LogLevel.INFO,
|
||||
mcpMode: true,
|
||||
logCallback: mockCallback
|
||||
});
|
||||
|
||||
logger.info('Test message');
|
||||
|
||||
expect(mockCallback).toHaveBeenCalledWith(
|
||||
'info',
|
||||
expect.stringContaining('Test message')
|
||||
);
|
||||
});
|
||||
|
||||
it('should silence logs when mcpMode=true and no callback', () => {
|
||||
const consoleSpy = vi.spyOn(console, 'log');
|
||||
const logger = new Logger({
|
||||
level: LogLevel.INFO,
|
||||
mcpMode: true
|
||||
// No callback
|
||||
});
|
||||
|
||||
logger.info('Test message');
|
||||
|
||||
expect(consoleSpy).not.toHaveBeenCalled();
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Child loggers', () => {
|
||||
it('should inherit callback from parent', () => {
|
||||
const mockCallback = vi.fn();
|
||||
const parent = new Logger({
|
||||
level: LogLevel.INFO,
|
||||
logCallback: mockCallback
|
||||
});
|
||||
|
||||
const child = parent.child('child');
|
||||
child.info('Child message');
|
||||
|
||||
expect(mockCallback).toHaveBeenCalledWith(
|
||||
'info',
|
||||
expect.stringContaining('[child]')
|
||||
);
|
||||
expect(mockCallback).toHaveBeenCalledWith(
|
||||
'info',
|
||||
expect.stringContaining('Child message')
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow child to override callback', () => {
|
||||
const parentCallback = vi.fn();
|
||||
const childCallback = vi.fn();
|
||||
|
||||
const parent = new Logger({
|
||||
level: LogLevel.INFO,
|
||||
logCallback: parentCallback
|
||||
});
|
||||
|
||||
const child = parent.child('child', {
|
||||
logCallback: childCallback
|
||||
});
|
||||
|
||||
parent.info('Parent message');
|
||||
child.info('Child message');
|
||||
|
||||
expect(parentCallback).toHaveBeenCalledTimes(1);
|
||||
expect(childCallback).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Configuration updates', () => {
|
||||
it('should allow updating logCallback via setConfig', () => {
|
||||
const callback1 = vi.fn();
|
||||
const callback2 = vi.fn();
|
||||
|
||||
const logger = new Logger({
|
||||
level: LogLevel.INFO,
|
||||
logCallback: callback1
|
||||
});
|
||||
|
||||
logger.info('Message 1');
|
||||
expect(callback1).toHaveBeenCalledTimes(1);
|
||||
|
||||
logger.setConfig({ logCallback: callback2 });
|
||||
logger.info('Message 2');
|
||||
|
||||
expect(callback1).toHaveBeenCalledTimes(1);
|
||||
expect(callback2).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should maintain mcpMode behavior when updating config', () => {
|
||||
const callback = vi.fn();
|
||||
const logger = new Logger({
|
||||
level: LogLevel.INFO,
|
||||
mcpMode: true
|
||||
});
|
||||
|
||||
// Initially silent (no callback)
|
||||
logger.info('Message 1');
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
|
||||
// Add callback - should start logging
|
||||
logger.setConfig({ logCallback: callback });
|
||||
logger.info('Message 2');
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Formatting with callback', () => {
|
||||
it('should include prefix in callback messages', () => {
|
||||
const mockCallback = vi.fn();
|
||||
const logger = new Logger({
|
||||
level: LogLevel.INFO,
|
||||
prefix: 'test-prefix',
|
||||
logCallback: mockCallback
|
||||
});
|
||||
|
||||
logger.info('Test message');
|
||||
|
||||
expect(mockCallback).toHaveBeenCalledWith(
|
||||
'info',
|
||||
expect.stringContaining('[test-prefix]')
|
||||
);
|
||||
});
|
||||
|
||||
it('should include timestamp when enabled', () => {
|
||||
const mockCallback = vi.fn();
|
||||
const logger = new Logger({
|
||||
level: LogLevel.INFO,
|
||||
timestamp: true,
|
||||
logCallback: mockCallback
|
||||
});
|
||||
|
||||
logger.info('Test message');
|
||||
|
||||
const [[, message]] = mockCallback.mock.calls;
|
||||
// Message should contain ISO timestamp pattern
|
||||
expect(message).toMatch(/\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
|
||||
});
|
||||
|
||||
it('should format additional arguments', () => {
|
||||
const mockCallback = vi.fn();
|
||||
const logger = new Logger({
|
||||
level: LogLevel.INFO,
|
||||
logCallback: mockCallback
|
||||
});
|
||||
|
||||
const data = { key: 'value' };
|
||||
logger.info('Test message', data, 'string arg');
|
||||
|
||||
expect(mockCallback).toHaveBeenCalledWith(
|
||||
'info',
|
||||
expect.stringContaining('Test message')
|
||||
);
|
||||
expect(mockCallback).toHaveBeenCalledWith(
|
||||
'info',
|
||||
expect.stringContaining('"key"')
|
||||
);
|
||||
expect(mockCallback).toHaveBeenCalledWith(
|
||||
'info',
|
||||
expect.stringContaining('string arg')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge cases', () => {
|
||||
it('should handle null/undefined callback gracefully', () => {
|
||||
const logger = new Logger({
|
||||
level: LogLevel.INFO,
|
||||
logCallback: undefined
|
||||
});
|
||||
|
||||
const consoleSpy = vi.spyOn(console, 'log');
|
||||
|
||||
// Should fallback to console
|
||||
logger.info('Test message');
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalled();
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should not call callback when level is SILENT', () => {
|
||||
const mockCallback = vi.fn();
|
||||
const logger = new Logger({
|
||||
level: LogLevel.SILENT,
|
||||
logCallback: mockCallback
|
||||
});
|
||||
|
||||
logger.error('Error');
|
||||
logger.warn('Warning');
|
||||
logger.info('Info');
|
||||
logger.debug('Debug');
|
||||
|
||||
expect(mockCallback).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should propagate callback errors', () => {
|
||||
const errorCallback: LogCallback = () => {
|
||||
throw new Error('Callback error');
|
||||
};
|
||||
|
||||
const logger = new Logger({
|
||||
level: LogLevel.INFO,
|
||||
logCallback: errorCallback
|
||||
});
|
||||
|
||||
// Should throw
|
||||
expect(() => {
|
||||
logger.info('Test message');
|
||||
}).toThrow('Callback error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Environment variable detection', () => {
|
||||
it('should detect MCP mode from environment', () => {
|
||||
const originalEnv = process.env.MCP_MODE;
|
||||
process.env.MCP_MODE = 'true';
|
||||
|
||||
const logger = new Logger({
|
||||
level: LogLevel.INFO
|
||||
});
|
||||
|
||||
const config = logger.getConfig();
|
||||
expect(config.mcpMode).toBe(true);
|
||||
expect(config.silent).toBe(true); // Should be silent without callback
|
||||
|
||||
// Cleanup
|
||||
if (originalEnv === undefined) {
|
||||
delete process.env.MCP_MODE;
|
||||
} else {
|
||||
process.env.MCP_MODE = originalEnv;
|
||||
}
|
||||
});
|
||||
|
||||
it('should detect log level from environment', () => {
|
||||
const originalEnv = process.env.TASK_MASTER_LOG_LEVEL;
|
||||
process.env.TASK_MASTER_LOG_LEVEL = 'DEBUG';
|
||||
|
||||
const logger = new Logger();
|
||||
const config = logger.getConfig();
|
||||
expect(config.level).toBe(LogLevel.DEBUG);
|
||||
|
||||
// Cleanup
|
||||
if (originalEnv === undefined) {
|
||||
delete process.env.TASK_MASTER_LOG_LEVEL;
|
||||
} else {
|
||||
process.env.TASK_MASTER_LOG_LEVEL = originalEnv;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -12,25 +12,52 @@ export enum LogLevel {
|
||||
DEBUG = 4
|
||||
}
|
||||
|
||||
/**
|
||||
* Log object interface (e.g., MCP context.log)
|
||||
*/
|
||||
export interface LogObject {
|
||||
info: (message: string) => void;
|
||||
warn: (message: string) => void;
|
||||
error: (message: string) => void;
|
||||
debug: (message: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log callback can be either a function or a log object
|
||||
*/
|
||||
export type LogCallback =
|
||||
| ((level: string, message: string) => void)
|
||||
| LogObject;
|
||||
|
||||
export interface LoggerConfig {
|
||||
level?: LogLevel;
|
||||
silent?: boolean;
|
||||
prefix?: string;
|
||||
timestamp?: boolean;
|
||||
colors?: boolean;
|
||||
// MCP mode silences all output
|
||||
// MCP mode silences all output (unless logCallback is provided)
|
||||
mcpMode?: boolean;
|
||||
// Callback function or object for logging (useful for MCP integration)
|
||||
logCallback?: LogCallback;
|
||||
}
|
||||
|
||||
export class Logger {
|
||||
private config: Required<LoggerConfig>;
|
||||
private static readonly DEFAULT_CONFIG: Required<LoggerConfig> = {
|
||||
private config: LoggerConfig & {
|
||||
level: LogLevel;
|
||||
silent: boolean;
|
||||
prefix: string;
|
||||
timestamp: boolean;
|
||||
colors: boolean;
|
||||
mcpMode: boolean;
|
||||
};
|
||||
private static readonly DEFAULT_CONFIG = {
|
||||
level: LogLevel.SILENT,
|
||||
silent: false,
|
||||
prefix: '',
|
||||
timestamp: false,
|
||||
colors: true,
|
||||
mcpMode: false
|
||||
mcpMode: false,
|
||||
logCallback: undefined as LogCallback | undefined
|
||||
};
|
||||
|
||||
constructor(config: LoggerConfig = {}) {
|
||||
@@ -80,8 +107,8 @@ export class Logger {
|
||||
...envConfig
|
||||
};
|
||||
|
||||
// MCP mode overrides everything to be silent
|
||||
if (this.config.mcpMode) {
|
||||
// MCP mode overrides to silent ONLY if no callback is provided
|
||||
if (this.config.mcpMode && !this.config.logCallback) {
|
||||
this.config.silent = true;
|
||||
}
|
||||
}
|
||||
@@ -90,6 +117,12 @@ export class Logger {
|
||||
* Check if logging is enabled for a given level
|
||||
*/
|
||||
private shouldLog(level: LogLevel): boolean {
|
||||
// If a callback is provided, route logs through it while still respecting the configured level
|
||||
if (this.config.logCallback) {
|
||||
return level <= this.config.level;
|
||||
}
|
||||
|
||||
// Otherwise, respect silent/mcpMode flags
|
||||
if (this.config.silent || this.config.mcpMode) {
|
||||
return false;
|
||||
}
|
||||
@@ -157,12 +190,66 @@ export class Logger {
|
||||
return formatted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if callback is a log object (has info/warn/error/debug methods)
|
||||
*/
|
||||
private isLogObject(callback: LogCallback): callback is LogObject {
|
||||
return (
|
||||
typeof callback === 'object' &&
|
||||
callback !== null &&
|
||||
'info' in callback &&
|
||||
'warn' in callback &&
|
||||
'error' in callback &&
|
||||
'debug' in callback
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Output a log message either to console or callback
|
||||
*/
|
||||
private output(
|
||||
level: LogLevel,
|
||||
levelName: string,
|
||||
message: string,
|
||||
...args: any[]
|
||||
): void {
|
||||
const formatted = this.formatMessage(level, message, ...args);
|
||||
|
||||
// Use callback if available
|
||||
if (this.config.logCallback) {
|
||||
// If callback is a log object, call the appropriate method
|
||||
if (this.isLogObject(this.config.logCallback)) {
|
||||
const method = levelName.toLowerCase() as keyof LogObject;
|
||||
if (method in this.config.logCallback) {
|
||||
this.config.logCallback[method](formatted);
|
||||
}
|
||||
} else {
|
||||
// Otherwise it's a function callback
|
||||
this.config.logCallback(levelName.toLowerCase(), formatted);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise use console
|
||||
switch (level) {
|
||||
case LogLevel.ERROR:
|
||||
console.error(formatted);
|
||||
break;
|
||||
case LogLevel.WARN:
|
||||
console.warn(formatted);
|
||||
break;
|
||||
default:
|
||||
console.log(formatted);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log an error message
|
||||
*/
|
||||
error(message: string, ...args: any[]): void {
|
||||
if (!this.shouldLog(LogLevel.ERROR)) return;
|
||||
console.error(this.formatMessage(LogLevel.ERROR, message, ...args));
|
||||
this.output(LogLevel.ERROR, 'ERROR', message, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -170,7 +257,7 @@ export class Logger {
|
||||
*/
|
||||
warn(message: string, ...args: any[]): void {
|
||||
if (!this.shouldLog(LogLevel.WARN)) return;
|
||||
console.warn(this.formatMessage(LogLevel.WARN, message, ...args));
|
||||
this.output(LogLevel.WARN, 'WARN', message, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -178,7 +265,7 @@ export class Logger {
|
||||
*/
|
||||
info(message: string, ...args: any[]): void {
|
||||
if (!this.shouldLog(LogLevel.INFO)) return;
|
||||
console.log(this.formatMessage(LogLevel.INFO, message, ...args));
|
||||
this.output(LogLevel.INFO, 'INFO', message, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -186,7 +273,7 @@ export class Logger {
|
||||
*/
|
||||
debug(message: string, ...args: any[]): void {
|
||||
if (!this.shouldLog(LogLevel.DEBUG)) return;
|
||||
console.log(this.formatMessage(LogLevel.DEBUG, message, ...args));
|
||||
this.output(LogLevel.DEBUG, 'DEBUG', message, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -194,6 +281,22 @@ export class Logger {
|
||||
* Useful for CLI output that should appear as-is
|
||||
*/
|
||||
log(message: string, ...args: any[]): void {
|
||||
// If callback is provided, use it for raw logs too
|
||||
if (this.config.logCallback) {
|
||||
const fullMessage =
|
||||
args.length > 0 ? [message, ...args].join(' ') : message;
|
||||
|
||||
// If callback is a log object, use info method for raw logs
|
||||
if (this.isLogObject(this.config.logCallback)) {
|
||||
this.config.logCallback.info(fullMessage);
|
||||
} else {
|
||||
// Otherwise it's a function callback
|
||||
this.config.logCallback('log', fullMessage);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, respect silent/mcpMode
|
||||
if (this.config.silent || this.config.mcpMode) return;
|
||||
|
||||
if (args.length > 0) {
|
||||
@@ -212,8 +315,8 @@ export class Logger {
|
||||
...config
|
||||
};
|
||||
|
||||
// MCP mode always overrides to silent
|
||||
if (this.config.mcpMode) {
|
||||
// MCP mode overrides to silent ONLY if no callback is provided
|
||||
if (this.config.mcpMode && !this.config.logCallback) {
|
||||
this.config.silent = true;
|
||||
}
|
||||
}
|
||||
@@ -221,7 +324,16 @@ export class Logger {
|
||||
/**
|
||||
* Get current configuration
|
||||
*/
|
||||
getConfig(): Readonly<Required<LoggerConfig>> {
|
||||
getConfig(): Readonly<
|
||||
LoggerConfig & {
|
||||
level: LogLevel;
|
||||
silent: boolean;
|
||||
prefix: string;
|
||||
timestamp: boolean;
|
||||
colors: boolean;
|
||||
mcpMode: boolean;
|
||||
}
|
||||
> {
|
||||
return { ...this.config };
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
/**
|
||||
* Public API for the executors module
|
||||
*/
|
||||
|
||||
export * from './types.js';
|
||||
export { BaseExecutor } from './base-executor.js';
|
||||
export { ClaudeExecutor } from './claude-executor.js';
|
||||
export { ExecutorFactory } from './executor-factory.js';
|
||||
export {
|
||||
ExecutorService,
|
||||
type ExecutorServiceOptions
|
||||
} from './executor-service.js';
|
||||
@@ -1,117 +1,127 @@
|
||||
/**
|
||||
* @fileoverview Main entry point for the tm-core package
|
||||
* This file exports all public APIs from the core Task Master library
|
||||
* @fileoverview Main entry point for @tm/core
|
||||
* Provides unified access to all Task Master functionality through TmCore
|
||||
*/
|
||||
|
||||
// Export main facade
|
||||
export {
|
||||
TaskMasterCore,
|
||||
createTaskMasterCore,
|
||||
type TaskMasterCoreOptions,
|
||||
type ListTasksResult,
|
||||
type StartTaskOptions,
|
||||
type StartTaskResult,
|
||||
type ConflictCheckResult,
|
||||
type ExportTasksOptions,
|
||||
type ExportResult
|
||||
} from './task-master-core.js';
|
||||
import type { TasksDomain } from './modules/tasks/tasks-domain.js';
|
||||
|
||||
// Re-export types
|
||||
export type * from './types/index.js';
|
||||
// ========== Primary API ==========
|
||||
|
||||
// Re-export interfaces (types only to avoid conflicts)
|
||||
export type * from './interfaces/index.js';
|
||||
/**
|
||||
* Create a new TmCore instance - The ONLY way to use tm-core
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { createTmCore } from '@tm/core';
|
||||
*
|
||||
* const tmcore = await createTmCore({
|
||||
* projectPath: process.cwd()
|
||||
* });
|
||||
*
|
||||
* // Access domains
|
||||
* await tmcore.auth.login({ ... });
|
||||
* const tasks = await tmcore.tasks.list();
|
||||
* await tmcore.workflow.start({ taskId: '1' });
|
||||
* await tmcore.git.commit('feat: add feature');
|
||||
* const config = tmcore.config.get('models.main');
|
||||
* ```
|
||||
*/
|
||||
export { createTmCore, TmCore, type TmCoreOptions } from './tm-core.js';
|
||||
|
||||
// Re-export constants
|
||||
export * from './constants/index.js';
|
||||
// ========== Type Exports ==========
|
||||
|
||||
// Re-export providers
|
||||
export * from './providers/index.js';
|
||||
// Common types that consumers need
|
||||
export type * from './common/types/index.js';
|
||||
|
||||
// Re-export storage (selectively to avoid conflicts)
|
||||
export {
|
||||
FileStorage,
|
||||
ApiStorage,
|
||||
StorageFactory,
|
||||
type ApiStorageConfig
|
||||
} from './storage/index.js';
|
||||
export { PlaceholderStorage, type StorageAdapter } from './storage/index.js';
|
||||
// Common interfaces
|
||||
export type * from './common/interfaces/index.js';
|
||||
|
||||
// Re-export parser
|
||||
export * from './parser/index.js';
|
||||
// Constants
|
||||
export * from './common/constants/index.js';
|
||||
|
||||
// Re-export utilities
|
||||
export * from './utils/index.js';
|
||||
// Errors
|
||||
export * from './common/errors/index.js';
|
||||
|
||||
// Re-export errors
|
||||
export * from './errors/index.js';
|
||||
// ========== Domain-Specific Type Exports ==========
|
||||
|
||||
// Re-export entities
|
||||
export { TaskEntity } from './entities/task.entity.js';
|
||||
// Task types
|
||||
export type {
|
||||
TaskListResult,
|
||||
GetTaskListOptions
|
||||
} from './modules/tasks/services/task-service.js';
|
||||
|
||||
// Re-export authentication
|
||||
export {
|
||||
AuthManager,
|
||||
AuthenticationError,
|
||||
type AuthCredentials,
|
||||
type OAuthFlowOptions,
|
||||
type AuthConfig
|
||||
} from './auth/index.js';
|
||||
export type {
|
||||
StartTaskOptions,
|
||||
StartTaskResult,
|
||||
ConflictCheckResult
|
||||
} from './modules/tasks/services/task-execution-service.js';
|
||||
|
||||
// Re-export logger
|
||||
export { getLogger, createLogger, setGlobalLogger } from './logger/index.js';
|
||||
export type {
|
||||
PreflightResult,
|
||||
CheckResult
|
||||
} from './modules/tasks/services/preflight-checker.service.js';
|
||||
|
||||
// Re-export executors
|
||||
export * from './executors/index.js';
|
||||
// Task domain result types
|
||||
export type TaskWithSubtaskResult = Awaited<ReturnType<TasksDomain['get']>>;
|
||||
|
||||
// Re-export reports
|
||||
export {
|
||||
ComplexityReportManager,
|
||||
type ComplexityReport,
|
||||
type ComplexityReportMetadata,
|
||||
type ComplexityAnalysis,
|
||||
type TaskComplexityData
|
||||
} from './reports/index.js';
|
||||
// Auth types
|
||||
export type {
|
||||
AuthCredentials,
|
||||
OAuthFlowOptions,
|
||||
UserContext
|
||||
} from './modules/auth/types.js';
|
||||
export { AuthenticationError } from './modules/auth/types.js';
|
||||
|
||||
// Re-export services
|
||||
export {
|
||||
PreflightChecker,
|
||||
TaskLoaderService,
|
||||
type CheckResult,
|
||||
type PreflightResult,
|
||||
type TaskValidationResult,
|
||||
type ValidationErrorType,
|
||||
type DependencyIssue
|
||||
} from './services/index.js';
|
||||
// Workflow types
|
||||
export type {
|
||||
StartWorkflowOptions,
|
||||
WorkflowStatus,
|
||||
NextAction
|
||||
} from './modules/workflow/services/workflow.service.js';
|
||||
|
||||
// Re-export Git adapter
|
||||
export { GitAdapter } from './git/git-adapter.js';
|
||||
export {
|
||||
CommitMessageGenerator,
|
||||
type CommitMessageOptions
|
||||
} from './git/commit-message-generator.js';
|
||||
|
||||
// Re-export workflow orchestrator, state manager, activity logger, and types
|
||||
export { WorkflowOrchestrator } from './workflow/workflow-orchestrator.js';
|
||||
export { WorkflowStateManager } from './workflow/workflow-state-manager.js';
|
||||
export { WorkflowActivityLogger } from './workflow/workflow-activity-logger.js';
|
||||
export type {
|
||||
WorkflowPhase,
|
||||
TDDPhase,
|
||||
WorkflowContext,
|
||||
WorkflowState,
|
||||
WorkflowEvent,
|
||||
WorkflowEventData,
|
||||
WorkflowEventListener,
|
||||
SubtaskInfo,
|
||||
TestResult,
|
||||
WorkflowError
|
||||
} from './workflow/types.js';
|
||||
TestResult
|
||||
} from './modules/workflow/types.js';
|
||||
|
||||
// Re-export workflow service
|
||||
export { WorkflowService } from './services/workflow.service.js';
|
||||
// Git types
|
||||
export type { CommitMessageOptions } from './modules/git/services/commit-message-generator.js';
|
||||
|
||||
// Integration types
|
||||
export type {
|
||||
StartWorkflowOptions,
|
||||
WorkflowStatus,
|
||||
NextAction
|
||||
} from './services/workflow.service.js';
|
||||
ExportTasksOptions,
|
||||
ExportResult
|
||||
} from './modules/integration/services/export.service.js';
|
||||
|
||||
// Reports types
|
||||
export type {
|
||||
ComplexityReport,
|
||||
ComplexityReportMetadata,
|
||||
ComplexityAnalysis,
|
||||
TaskComplexityData
|
||||
} from './modules/reports/types.js';
|
||||
|
||||
// ========== Advanced API (for CLI/Extension/MCP) ==========
|
||||
|
||||
// Auth - Advanced
|
||||
export { AuthManager } from './modules/auth/managers/auth-manager.js';
|
||||
|
||||
// Workflow - Advanced
|
||||
export { WorkflowOrchestrator } from './modules/workflow/orchestrators/workflow-orchestrator.js';
|
||||
export { WorkflowStateManager } from './modules/workflow/managers/workflow-state-manager.js';
|
||||
export { WorkflowService } from './modules/workflow/services/workflow.service.js';
|
||||
export type { SubtaskInfo } from './modules/workflow/types.js';
|
||||
|
||||
// Git - Advanced
|
||||
export { GitAdapter } from './modules/git/adapters/git-adapter.js';
|
||||
export { CommitMessageGenerator } from './modules/git/services/commit-message-generator.js';
|
||||
|
||||
// Tasks - Advanced
|
||||
export { PreflightChecker } from './modules/tasks/services/preflight-checker.service.js';
|
||||
export { TaskLoaderService } from './modules/tasks/services/task-loader.service.js';
|
||||
|
||||
// Integration - Advanced
|
||||
export { ExportService } from './modules/integration/services/export.service.js';
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
// Export all from AI module
|
||||
export * from './ai/index.js';
|
||||
export * from './providers/index.js';
|
||||
|
||||
// Storage providers will be exported here when implemented
|
||||
// export * from './storage/index.js';
|
||||
@@ -6,12 +6,15 @@
|
||||
import {
|
||||
ERROR_CODES,
|
||||
TaskMasterError
|
||||
} from '../../errors/task-master-error.js';
|
||||
} from '../../../common/errors/task-master-error.js';
|
||||
import type {
|
||||
AIOptions,
|
||||
AIResponse,
|
||||
IAIProvider
|
||||
} from '../../interfaces/ai-provider.interface.js';
|
||||
IAIProvider,
|
||||
ProviderUsageStats,
|
||||
ProviderInfo,
|
||||
AIModel
|
||||
} from '../interfaces/ai-provider.interface.js';
|
||||
|
||||
// Constants for retry logic
|
||||
const DEFAULT_MAX_RETRIES = 3;
|
||||
@@ -428,17 +431,10 @@ export abstract class BaseProvider implements IAIProvider {
|
||||
options?: AIOptions
|
||||
): AsyncIterator<Partial<AIResponse>>;
|
||||
abstract isAvailable(): Promise<boolean>;
|
||||
abstract getProviderInfo(): import(
|
||||
'../../interfaces/ai-provider.interface.js'
|
||||
).ProviderInfo;
|
||||
abstract getAvailableModels(): import(
|
||||
'../../interfaces/ai-provider.interface.js'
|
||||
).AIModel[];
|
||||
abstract getProviderInfo(): ProviderInfo;
|
||||
abstract getAvailableModels(): AIModel[];
|
||||
abstract validateCredentials(): Promise<boolean>;
|
||||
abstract getUsageStats(): Promise<
|
||||
| import('../../interfaces/ai-provider.interface.js').ProviderUsageStats
|
||||
| null
|
||||
>;
|
||||
abstract getUsageStats(): Promise<ProviderUsageStats | null>;
|
||||
abstract initialize(): Promise<void>;
|
||||
abstract close(): Promise<void>;
|
||||
}
|
||||
208
packages/tm-core/src/modules/auth/auth-domain.ts
Normal file
208
packages/tm-core/src/modules/auth/auth-domain.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* @fileoverview Auth Domain Facade
|
||||
* Public API for authentication and authorization
|
||||
*/
|
||||
|
||||
import path from 'node:path';
|
||||
import { AuthManager } from './managers/auth-manager.js';
|
||||
import type {
|
||||
AuthCredentials,
|
||||
OAuthFlowOptions,
|
||||
UserContext
|
||||
} from './types.js';
|
||||
import type {
|
||||
Organization,
|
||||
Brief,
|
||||
RemoteTask
|
||||
} from './services/organization.service.js';
|
||||
import type { StorageType } from '../../common/types/index.js';
|
||||
|
||||
/**
|
||||
* Display information for storage context
|
||||
*/
|
||||
export interface StorageDisplayInfo {
|
||||
storageType: Exclude<StorageType, 'auto'>;
|
||||
briefInfo?: {
|
||||
briefId: string;
|
||||
briefName: string;
|
||||
orgSlug?: string;
|
||||
webAppUrl?: string;
|
||||
};
|
||||
filePath?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Auth Domain - Unified API for authentication operations
|
||||
*/
|
||||
export class AuthDomain {
|
||||
private authManager: AuthManager;
|
||||
|
||||
constructor() {
|
||||
this.authManager = AuthManager.getInstance();
|
||||
}
|
||||
|
||||
// ========== Authentication ==========
|
||||
|
||||
/**
|
||||
* Check if user is authenticated
|
||||
*/
|
||||
isAuthenticated(): boolean {
|
||||
return this.authManager.isAuthenticated();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stored credentials
|
||||
*/
|
||||
getCredentials(): AuthCredentials | null {
|
||||
return this.authManager.getCredentials();
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate with OAuth flow
|
||||
*/
|
||||
async authenticateWithOAuth(
|
||||
options?: OAuthFlowOptions
|
||||
): Promise<AuthCredentials> {
|
||||
return this.authManager.authenticateWithOAuth(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get OAuth authorization URL
|
||||
*/
|
||||
getAuthorizationUrl(): string | null {
|
||||
return this.authManager.getAuthorizationUrl();
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh authentication token
|
||||
*/
|
||||
async refreshToken(): Promise<AuthCredentials> {
|
||||
return this.authManager.refreshToken();
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout current user
|
||||
*/
|
||||
async logout(): Promise<void> {
|
||||
return this.authManager.logout();
|
||||
}
|
||||
|
||||
// ========== User Context Management ==========
|
||||
|
||||
/**
|
||||
* Get current user context (org/brief selection)
|
||||
*/
|
||||
getContext(): UserContext | null {
|
||||
return this.authManager.getContext();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update user context
|
||||
*/
|
||||
updateContext(context: Partial<UserContext>): void {
|
||||
return this.authManager.updateContext(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear user context
|
||||
*/
|
||||
clearContext(): void {
|
||||
return this.authManager.clearContext();
|
||||
}
|
||||
|
||||
// ========== Organization Management ==========
|
||||
|
||||
/**
|
||||
* Get all organizations for the authenticated user
|
||||
*/
|
||||
async getOrganizations(): Promise<Organization[]> {
|
||||
return this.authManager.getOrganizations();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific organization by ID
|
||||
*/
|
||||
async getOrganization(orgId: string): Promise<Organization | null> {
|
||||
return this.authManager.getOrganization(orgId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all briefs for a specific organization
|
||||
*/
|
||||
async getBriefs(orgId: string): Promise<Brief[]> {
|
||||
return this.authManager.getBriefs(orgId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific brief by ID
|
||||
*/
|
||||
async getBrief(briefId: string): Promise<Brief | null> {
|
||||
return this.authManager.getBrief(briefId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all tasks for a specific brief
|
||||
*/
|
||||
async getTasks(briefId: string): Promise<RemoteTask[]> {
|
||||
return this.authManager.getTasks(briefId);
|
||||
}
|
||||
|
||||
// ========== Display Information ==========
|
||||
|
||||
/**
|
||||
* Get storage display information for UI presentation
|
||||
* Includes brief info for API storage, file path for file storage
|
||||
*
|
||||
* @param resolvedStorageType - The actual storage type being used at runtime.
|
||||
* Get this from tmCore.tasks.getStorageType()
|
||||
*/
|
||||
getStorageDisplayInfo(
|
||||
resolvedStorageType: 'file' | 'api'
|
||||
): StorageDisplayInfo {
|
||||
if (resolvedStorageType === 'api') {
|
||||
const context = this.getContext();
|
||||
if (context?.briefId && context?.briefName) {
|
||||
return {
|
||||
storageType: 'api',
|
||||
briefInfo: {
|
||||
briefId: context.briefId,
|
||||
briefName: context.briefName,
|
||||
orgSlug: context.orgSlug,
|
||||
webAppUrl: this.getWebAppUrl()
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Default to file storage display
|
||||
return {
|
||||
storageType: 'file',
|
||||
filePath: path.join('.taskmaster', 'tasks', 'tasks.json')
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get web app base URL from environment configuration
|
||||
* @private
|
||||
*/
|
||||
private getWebAppUrl(): string | undefined {
|
||||
const baseDomain =
|
||||
process.env.TM_BASE_DOMAIN || process.env.TM_PUBLIC_BASE_DOMAIN;
|
||||
|
||||
if (!baseDomain) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// If it already includes protocol, use as-is
|
||||
if (baseDomain.startsWith('http://') || baseDomain.startsWith('https://')) {
|
||||
return baseDomain;
|
||||
}
|
||||
|
||||
// Otherwise, add protocol based on domain
|
||||
if (baseDomain.includes('localhost') || baseDomain.includes('127.0.0.1')) {
|
||||
return `http://${baseDomain}`;
|
||||
}
|
||||
|
||||
return `https://${baseDomain}`;
|
||||
}
|
||||
}
|
||||
29
packages/tm-core/src/modules/auth/index.ts
Normal file
29
packages/tm-core/src/modules/auth/index.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Authentication module exports
|
||||
*/
|
||||
|
||||
export { AuthDomain, type StorageDisplayInfo } from './auth-domain.js';
|
||||
export { AuthManager } from './managers/auth-manager.js';
|
||||
export { CredentialStore } from './services/credential-store.js';
|
||||
export { OAuthService } from './services/oauth-service.js';
|
||||
export { SupabaseSessionStorage } from './services/supabase-session-storage.js';
|
||||
export type {
|
||||
Organization,
|
||||
Brief,
|
||||
RemoteTask
|
||||
} from './services/organization.service.js';
|
||||
|
||||
export type {
|
||||
AuthCredentials,
|
||||
OAuthFlowOptions,
|
||||
AuthConfig,
|
||||
CliData,
|
||||
UserContext
|
||||
} from './types.js';
|
||||
|
||||
export { AuthenticationError } from './types.js';
|
||||
|
||||
export {
|
||||
DEFAULT_AUTH_CONFIG,
|
||||
getAuthConfig
|
||||
} from './config.js';
|
||||
@@ -8,17 +8,17 @@ import {
|
||||
AuthenticationError,
|
||||
AuthConfig,
|
||||
UserContext
|
||||
} from './types.js';
|
||||
import { CredentialStore } from './credential-store.js';
|
||||
import { OAuthService } from './oauth-service.js';
|
||||
import { SupabaseAuthClient } from '../clients/supabase-client.js';
|
||||
} from '../types.js';
|
||||
import { CredentialStore } from '../services/credential-store.js';
|
||||
import { OAuthService } from '../services/oauth-service.js';
|
||||
import { SupabaseAuthClient } from '../../integration/clients/supabase-client.js';
|
||||
import {
|
||||
OrganizationService,
|
||||
type Organization,
|
||||
type Brief,
|
||||
type RemoteTask
|
||||
} from '../services/organization.service.js';
|
||||
import { getLogger } from '../logger/index.js';
|
||||
import { getLogger } from '../../../common/logger/index.js';
|
||||
|
||||
/**
|
||||
* Authentication manager class
|
||||
@@ -28,7 +28,7 @@ export class AuthManager {
|
||||
private static readonly staticLogger = getLogger('AuthManager');
|
||||
private credentialStore: CredentialStore;
|
||||
private oauthService: OAuthService;
|
||||
private supabaseClient: SupabaseAuthClient;
|
||||
public supabaseClient: SupabaseAuthClient;
|
||||
private organizationService?: OrganizationService;
|
||||
private readonly logger = getLogger('AuthManager');
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
||||
import { CredentialStore } from './credential-store.js';
|
||||
import { AuthenticationError } from './types.js';
|
||||
import type { AuthCredentials } from './types.js';
|
||||
import { CredentialStore } from '../services/credential-store.js';
|
||||
import { AuthenticationError } from '../types.js';
|
||||
import type { AuthCredentials } from '../types.js';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
@@ -4,9 +4,9 @@
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { AuthCredentials, AuthenticationError, AuthConfig } from './types.js';
|
||||
import { getAuthConfig } from './config.js';
|
||||
import { getLogger } from '../logger/index.js';
|
||||
import { AuthCredentials, AuthenticationError, AuthConfig } from '../types.js';
|
||||
import { getAuthConfig } from '../config.js';
|
||||
import { getLogger } from '../../../common/logger/index.js';
|
||||
|
||||
/**
|
||||
* CredentialStore manages the persistence and retrieval of authentication credentials.
|
||||
@@ -12,12 +12,12 @@ import {
|
||||
OAuthFlowOptions,
|
||||
AuthConfig,
|
||||
CliData
|
||||
} from './types.js';
|
||||
import { CredentialStore } from './credential-store.js';
|
||||
import { SupabaseAuthClient } from '../clients/supabase-client.js';
|
||||
import { getAuthConfig } from './config.js';
|
||||
import { getLogger } from '../logger/index.js';
|
||||
import packageJson from '../../../../package.json' with { type: 'json' };
|
||||
} from '../types.js';
|
||||
import { CredentialStore } from '../services/credential-store.js';
|
||||
import { SupabaseAuthClient } from '../../integration/clients/supabase-client.js';
|
||||
import { getAuthConfig } from '../config.js';
|
||||
import { getLogger } from '../../../common/logger/index.js';
|
||||
import packageJson from '../../../../../../package.json' with { type: 'json' };
|
||||
|
||||
export class OAuthService {
|
||||
private logger = getLogger('OAuthService');
|
||||
@@ -4,9 +4,12 @@
|
||||
*/
|
||||
|
||||
import { SupabaseClient } from '@supabase/supabase-js';
|
||||
import { Database } from '../types/database.types.js';
|
||||
import { TaskMasterError, ERROR_CODES } from '../errors/task-master-error.js';
|
||||
import { getLogger } from '../logger/index.js';
|
||||
import { Database } from '../../../common/types/database.types.js';
|
||||
import {
|
||||
TaskMasterError,
|
||||
ERROR_CODES
|
||||
} from '../../../common/errors/task-master-error.js';
|
||||
import { getLogger } from '../../../common/logger/index.js';
|
||||
|
||||
/**
|
||||
* Organization data structure
|
||||
@@ -6,10 +6,10 @@
|
||||
* auth.json credential storage, maintaining backward compatibility
|
||||
*/
|
||||
|
||||
import { SupportedStorage } from '@supabase/supabase-js';
|
||||
import type { SupportedStorage } from '@supabase/supabase-js';
|
||||
import { CredentialStore } from './credential-store.js';
|
||||
import { AuthCredentials } from './types.js';
|
||||
import { getLogger } from '../logger/index.js';
|
||||
import type { AuthCredentials } from '../types.js';
|
||||
import { getLogger } from '../../../common/logger/index.js';
|
||||
|
||||
const STORAGE_KEY = 'sb-taskmaster-auth-token';
|
||||
|
||||
@@ -29,20 +29,14 @@ export class SupabaseSessionStorage implements SupportedStorage {
|
||||
const session = {
|
||||
access_token: credentials.token,
|
||||
refresh_token: credentials.refreshToken || '',
|
||||
expires_at: credentials.expiresAt
|
||||
? Math.floor(new Date(credentials.expiresAt).getTime() / 1000)
|
||||
: Math.floor(Date.now() / 1000) + 3600, // Default to 1 hour
|
||||
// Don't default to arbitrary values - let Supabase handle refresh
|
||||
...(credentials.expiresAt && {
|
||||
expires_at: Math.floor(new Date(credentials.expiresAt).getTime() / 1000)
|
||||
}),
|
||||
token_type: 'bearer',
|
||||
user: {
|
||||
id: credentials.userId,
|
||||
email: credentials.email || '',
|
||||
aud: 'authenticated',
|
||||
role: 'authenticated',
|
||||
email_confirmed_at: new Date().toISOString(),
|
||||
app_metadata: {},
|
||||
user_metadata: {},
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
email: credentials.email || ''
|
||||
}
|
||||
};
|
||||
return session;
|
||||
@@ -55,11 +49,14 @@ export class SupabaseSessionStorage implements SupportedStorage {
|
||||
sessionData: any
|
||||
): Partial<AuthCredentials> {
|
||||
try {
|
||||
const session = JSON.parse(sessionData);
|
||||
// Handle both string and object formats (Supabase may pass either)
|
||||
const session =
|
||||
typeof sessionData === 'string' ? JSON.parse(sessionData) : sessionData;
|
||||
|
||||
return {
|
||||
token: session.access_token,
|
||||
refreshToken: session.refresh_token,
|
||||
userId: session.user?.id || 'unknown',
|
||||
userId: session.user?.id,
|
||||
email: session.user?.email,
|
||||
expiresAt: session.expires_at
|
||||
? new Date(session.expires_at * 1000).toISOString()
|
||||
@@ -78,21 +75,29 @@ export class SupabaseSessionStorage implements SupportedStorage {
|
||||
// Supabase uses a specific key pattern for sessions
|
||||
if (key === STORAGE_KEY || key.includes('auth-token')) {
|
||||
try {
|
||||
const credentials = this.store.getCredentials({ allowExpired: true });
|
||||
if (credentials && credentials.token) {
|
||||
// Build and return a session object from our stored credentials
|
||||
const session = this.buildSessionFromCredentials(credentials);
|
||||
return JSON.stringify(session);
|
||||
// Get credentials and let Supabase handle expiry/refresh internally
|
||||
const credentials = this.store.getCredentials();
|
||||
|
||||
// Only return a session if we have BOTH access token AND refresh token
|
||||
// Supabase will handle refresh if session is expired
|
||||
if (!credentials?.token || !credentials?.refreshToken) {
|
||||
this.logger.debug('No valid credentials found');
|
||||
return null;
|
||||
}
|
||||
|
||||
const session = this.buildSessionFromCredentials(credentials);
|
||||
return JSON.stringify(session);
|
||||
} catch (error) {
|
||||
this.logger.error('Error getting session:', error);
|
||||
}
|
||||
}
|
||||
// Return null if no valid session exists - Supabase expects this
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set item in storage - Supabase will store the session with a specific key
|
||||
* CRITICAL: This is called during refresh token rotation - must be atomic
|
||||
*/
|
||||
setItem(key: string, value: string): void {
|
||||
// Only handle Supabase session keys
|
||||
@@ -102,21 +107,64 @@ export class SupabaseSessionStorage implements SupportedStorage {
|
||||
|
||||
// Parse the session and update our credentials
|
||||
const sessionUpdates = this.parseSessionToCredentials(value);
|
||||
const existingCredentials = this.store.getCredentials();
|
||||
const existingCredentials = this.store.getCredentials({
|
||||
allowExpired: true
|
||||
});
|
||||
|
||||
if (sessionUpdates.token) {
|
||||
const updatedCredentials: AuthCredentials = {
|
||||
...existingCredentials,
|
||||
...sessionUpdates,
|
||||
savedAt: new Date().toISOString(),
|
||||
selectedContext: existingCredentials?.selectedContext
|
||||
} as AuthCredentials;
|
||||
// CRITICAL: Only save if we have both tokens - prevents partial session states
|
||||
// Refresh token rotation means we MUST persist the new refresh token immediately
|
||||
if (!sessionUpdates.token || !sessionUpdates.refreshToken) {
|
||||
this.logger.warn(
|
||||
'Received incomplete session update - skipping save to prevent token rotation issues',
|
||||
{
|
||||
hasToken: !!sessionUpdates.token,
|
||||
hasRefreshToken: !!sessionUpdates.refreshToken
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.store.saveCredentials(updatedCredentials);
|
||||
this.logger.info(
|
||||
'Successfully saved refreshed credentials from Supabase'
|
||||
// Log the refresh token rotation for debugging
|
||||
const isRotation =
|
||||
existingCredentials?.refreshToken !== sessionUpdates.refreshToken;
|
||||
if (isRotation) {
|
||||
this.logger.debug(
|
||||
'Refresh token rotated - storing new refresh token atomically'
|
||||
);
|
||||
}
|
||||
|
||||
// Build updated credentials - ATOMIC update of both tokens
|
||||
const userId = sessionUpdates.userId ?? existingCredentials?.userId;
|
||||
|
||||
// Runtime assertion: userId is required for AuthCredentials
|
||||
if (!userId) {
|
||||
this.logger.error(
|
||||
'Cannot save credentials: userId is missing from both session update and existing credentials'
|
||||
);
|
||||
throw new Error('Invalid session state: userId is required');
|
||||
}
|
||||
|
||||
const updatedCredentials: AuthCredentials = {
|
||||
...(existingCredentials ?? {}),
|
||||
token: sessionUpdates.token,
|
||||
refreshToken: sessionUpdates.refreshToken,
|
||||
expiresAt: sessionUpdates.expiresAt,
|
||||
userId,
|
||||
email: sessionUpdates.email ?? existingCredentials?.email,
|
||||
savedAt: new Date().toISOString(),
|
||||
selectedContext: existingCredentials?.selectedContext
|
||||
} as AuthCredentials;
|
||||
|
||||
// Save synchronously to ensure atomicity during refresh
|
||||
this.store.saveCredentials(updatedCredentials);
|
||||
|
||||
this.logger.info(
|
||||
'Successfully saved refreshed credentials from Supabase',
|
||||
{
|
||||
tokenRotated: isRotation,
|
||||
expiresAt: updatedCredentials.expiresAt
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error('Error setting session:', error);
|
||||
}
|
||||
8
packages/tm-core/src/modules/commands/index.ts
Normal file
8
packages/tm-core/src/modules/commands/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* @fileoverview Commands domain - Placeholder for future migration
|
||||
* This module will handle command execution and orchestration
|
||||
* when migrated from scripts/modules/commands.js
|
||||
*/
|
||||
|
||||
// TODO: Migrate commands.js from scripts/modules/
|
||||
// export * from './handlers/command-handler.js';
|
||||
116
packages/tm-core/src/modules/config/config-domain.ts
Normal file
116
packages/tm-core/src/modules/config/config-domain.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* @fileoverview Config Domain Facade
|
||||
* Public API for configuration management
|
||||
*/
|
||||
|
||||
import type { ConfigManager } from './managers/config-manager.js';
|
||||
import type {
|
||||
PartialConfiguration,
|
||||
RuntimeStorageConfig
|
||||
} from '../../common/interfaces/configuration.interface.js';
|
||||
|
||||
/**
|
||||
* Config Domain - Unified API for configuration operations
|
||||
*/
|
||||
export class ConfigDomain {
|
||||
constructor(private configManager: ConfigManager) {}
|
||||
|
||||
// ========== Configuration Access ==========
|
||||
|
||||
/**
|
||||
* Get the full configuration
|
||||
*/
|
||||
getConfig(): PartialConfiguration {
|
||||
return this.configManager.getConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get storage configuration
|
||||
*/
|
||||
getStorageConfig(): RuntimeStorageConfig {
|
||||
return this.configManager.getStorageConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get model configuration
|
||||
*/
|
||||
getModelConfig() {
|
||||
return this.configManager.getModelConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get response language
|
||||
*/
|
||||
getResponseLanguage(): string {
|
||||
return this.configManager.getResponseLanguage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get project root path
|
||||
*/
|
||||
getProjectRoot(): string {
|
||||
return this.configManager.getProjectRoot();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if API is explicitly configured
|
||||
*/
|
||||
isApiExplicitlyConfigured(): boolean {
|
||||
return this.configManager.isApiExplicitlyConfigured();
|
||||
}
|
||||
|
||||
// ========== Runtime State ==========
|
||||
|
||||
/**
|
||||
* Get the currently active tag
|
||||
*/
|
||||
getActiveTag(): string {
|
||||
return this.configManager.getActiveTag();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the active tag
|
||||
*/
|
||||
async setActiveTag(tag: string): Promise<void> {
|
||||
return this.configManager.setActiveTag(tag);
|
||||
}
|
||||
|
||||
// ========== Configuration Updates ==========
|
||||
|
||||
/**
|
||||
* Update configuration
|
||||
*/
|
||||
async updateConfig(updates: PartialConfiguration): Promise<void> {
|
||||
return this.configManager.updateConfig(updates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set response language
|
||||
*/
|
||||
async setResponseLanguage(language: string): Promise<void> {
|
||||
return this.configManager.setResponseLanguage(language);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save current configuration
|
||||
*/
|
||||
async saveConfig(): Promise<void> {
|
||||
return this.configManager.saveConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset configuration to defaults
|
||||
*/
|
||||
async reset(): Promise<void> {
|
||||
return this.configManager.reset();
|
||||
}
|
||||
|
||||
// ========== Utilities ==========
|
||||
|
||||
/**
|
||||
* Get configuration sources for debugging
|
||||
*/
|
||||
getConfigSources() {
|
||||
return this.configManager.getConfigSources();
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
// Export the main ConfigManager
|
||||
export { ConfigManager } from './config-manager.js';
|
||||
export { ConfigManager } from './managers/config-manager.js';
|
||||
|
||||
// Export all configuration services for advanced usage
|
||||
export {
|
||||
@@ -38,7 +38,7 @@ export type {
|
||||
ConfigProperty,
|
||||
IConfigurationFactory,
|
||||
IConfigurationManager
|
||||
} from '../interfaces/configuration.interface.js';
|
||||
} from '../../common/interfaces/configuration.interface.js';
|
||||
|
||||
// Re-export default values
|
||||
export { DEFAULT_CONFIG_VALUES } from '../interfaces/configuration.interface.js';
|
||||
export { DEFAULT_CONFIG_VALUES } from '../../common/interfaces/configuration.interface.js';
|
||||
@@ -5,19 +5,19 @@
|
||||
|
||||
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
||||
import { ConfigManager } from './config-manager.js';
|
||||
import { DEFAULT_CONFIG_VALUES } from '../interfaces/configuration.interface.js';
|
||||
import { ConfigLoader } from './services/config-loader.service.js';
|
||||
import { ConfigMerger } from './services/config-merger.service.js';
|
||||
import { RuntimeStateManager } from './services/runtime-state-manager.service.js';
|
||||
import { ConfigPersistence } from './services/config-persistence.service.js';
|
||||
import { EnvironmentConfigProvider } from './services/environment-config-provider.service.js';
|
||||
import { DEFAULT_CONFIG_VALUES } from '../../../common/interfaces/configuration.interface.js';
|
||||
import { ConfigLoader } from '../services/config-loader.service.js';
|
||||
import { ConfigMerger } from '../services/config-merger.service.js';
|
||||
import { RuntimeStateManager } from '../services/runtime-state-manager.service.js';
|
||||
import { ConfigPersistence } from '../services/config-persistence.service.js';
|
||||
import { EnvironmentConfigProvider } from '../services/environment-config-provider.service.js';
|
||||
|
||||
// Mock all services
|
||||
vi.mock('./services/config-loader.service.js');
|
||||
vi.mock('./services/config-merger.service.js');
|
||||
vi.mock('./services/runtime-state-manager.service.js');
|
||||
vi.mock('./services/config-persistence.service.js');
|
||||
vi.mock('./services/environment-config-provider.service.js');
|
||||
vi.mock('../services/config-loader.service.js');
|
||||
vi.mock('../services/config-merger.service.js');
|
||||
vi.mock('../services/runtime-state-manager.service.js');
|
||||
vi.mock('../services/config-persistence.service.js');
|
||||
vi.mock('../services/environment-config-provider.service.js');
|
||||
|
||||
describe('ConfigManager', () => {
|
||||
let manager: ConfigManager;
|
||||
@@ -361,23 +361,6 @@ describe('ConfigManager', () => {
|
||||
|
||||
expect(sources).toEqual(mockSources);
|
||||
});
|
||||
|
||||
it('should return no-op function for watch (not implemented)', () => {
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
const callback = vi.fn();
|
||||
|
||||
const unsubscribe = manager.watch(callback);
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
'Configuration watching not yet implemented'
|
||||
);
|
||||
expect(unsubscribe).toBeInstanceOf(Function);
|
||||
|
||||
// Calling unsubscribe should not throw
|
||||
expect(() => unsubscribe()).not.toThrow();
|
||||
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
@@ -9,16 +9,16 @@
|
||||
import type {
|
||||
PartialConfiguration,
|
||||
RuntimeStorageConfig
|
||||
} from '../interfaces/configuration.interface.js';
|
||||
import { DEFAULT_CONFIG_VALUES as DEFAULTS } from '../interfaces/configuration.interface.js';
|
||||
import { ConfigLoader } from './services/config-loader.service.js';
|
||||
} from '../../../common/interfaces/configuration.interface.js';
|
||||
import { DEFAULT_CONFIG_VALUES as DEFAULTS } from '../../../common/interfaces/configuration.interface.js';
|
||||
import { ConfigLoader } from '../services/config-loader.service.js';
|
||||
import {
|
||||
ConfigMerger,
|
||||
CONFIG_PRECEDENCE
|
||||
} from './services/config-merger.service.js';
|
||||
import { RuntimeStateManager } from './services/runtime-state-manager.service.js';
|
||||
import { ConfigPersistence } from './services/config-persistence.service.js';
|
||||
import { EnvironmentConfigProvider } from './services/environment-config-provider.service.js';
|
||||
} from '../services/config-merger.service.js';
|
||||
import { RuntimeStateManager } from '../services/runtime-state-manager.service.js';
|
||||
import { ConfigPersistence } from '../services/config-persistence.service.js';
|
||||
import { EnvironmentConfigProvider } from '../services/environment-config-provider.service.js';
|
||||
|
||||
/**
|
||||
* ConfigManager orchestrates all configuration services
|
||||
@@ -3,9 +3,9 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
||||
import { promises as fs } from 'node:fs';
|
||||
import fs from 'node:fs/promises';
|
||||
import { ConfigLoader } from './config-loader.service.js';
|
||||
import { DEFAULT_CONFIG_VALUES } from '../../interfaces/configuration.interface.js';
|
||||
import { DEFAULT_CONFIG_VALUES } from '../../../common/interfaces/configuration.interface.js';
|
||||
|
||||
vi.mock('node:fs', () => ({
|
||||
promises: {
|
||||
@@ -3,14 +3,14 @@
|
||||
* Responsible for loading configuration from various file sources
|
||||
*/
|
||||
|
||||
import { promises as fs } from 'node:fs';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import type { PartialConfiguration } from '../../interfaces/configuration.interface.js';
|
||||
import { DEFAULT_CONFIG_VALUES } from '../../interfaces/configuration.interface.js';
|
||||
import type { PartialConfiguration } from '../../../common/interfaces/configuration.interface.js';
|
||||
import { DEFAULT_CONFIG_VALUES } from '../../../common/interfaces/configuration.interface.js';
|
||||
import {
|
||||
ERROR_CODES,
|
||||
TaskMasterError
|
||||
} from '../../errors/task-master-error.js';
|
||||
} from '../../../common/errors/task-master-error.js';
|
||||
|
||||
/**
|
||||
* ConfigLoader handles loading configuration from files
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user