Merge pull request #1348 from eyaltoledano/next (Release 0.30.1)

This commit is contained in:
Ralph Khreish
2025-10-27 18:15:28 +01:00
committed by GitHub
178 changed files with 5569 additions and 1776 deletions

View File

@@ -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

View 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
View 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"
]
}

View File

@@ -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

View File

@@ -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 }}"

View File

@@ -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
}
},

View File

@@ -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"

View File

@@ -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

View File

@@ -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/`

View File

@@ -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';

View File

@@ -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;
}
}

View File

@@ -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) {

View File

@@ -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';

View File

@@ -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,

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -41,5 +41,5 @@ export type {
Task,
TaskStatus,
TaskPriority,
TaskMasterCore
TmCore
} from '@tm/core';

View File

@@ -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';
/**

View File

@@ -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';
/**

View File

@@ -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,

View File

@@ -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
});
}

View File

@@ -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)

View File

@@ -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

View File

@@ -61,7 +61,7 @@ npm run build:css
npm run build
# Type checking
npm run check-types
npm run typecheck
# Linting
npm run lint

View File

@@ -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",

View File

@@ -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;
}
}
}

View File

@@ -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';

View File

@@ -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}`;

View File

@@ -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,

View 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
});
}
}
)
});
}

View 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
});
}
}
)
});
}

View 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';

View File

@@ -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
}
};
}
}

View File

@@ -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
}
};
}
}

View File

@@ -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,

View File

@@ -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: {

View File

@@ -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,

View File

@@ -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}`);
}
})
});
}

View File

@@ -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

View File

@@ -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
View File

@@ -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": {

View File

@@ -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",

View 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.

View 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"
}

View 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';

View 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;
}
}

View 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"]
}

View File

@@ -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';

View File

@@ -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';

View File

@@ -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

View File

@@ -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';

View 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;
}
});
});
});

View File

@@ -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 };
}

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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>;
}

View 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}`;
}
}

View 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';

View File

@@ -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');

View File

@@ -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';

View File

@@ -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.

View File

@@ -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');

View File

@@ -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

View File

@@ -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);
}

View 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';

View 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();
}
}

View File

@@ -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';

View File

@@ -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', () => {

View File

@@ -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

View File

@@ -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: {

View File

@@ -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