feat: implement local-only command checkers for cli and mcp (#1426)

This commit is contained in:
Ralph Khreish
2025-11-19 22:08:04 +01:00
committed by GitHub
parent 99d9179522
commit 4049f34d5a
57 changed files with 2676 additions and 482 deletions

View File

@@ -1,21 +1,28 @@
import boxen from 'boxen'; import boxen from 'boxen';
import chalk from 'chalk'; import chalk from 'chalk';
/**
* Level/variant for the card box styling
*/
export type CardBoxLevel = 'warn' | 'info';
/** /**
* Configuration for the card box component * Configuration for the card box component
*/ */
export interface CardBoxConfig { export interface CardBoxConfig {
/** Header text displayed in yellow bold */ /** Header text displayed in bold */
header: string; header: string;
/** Body paragraphs displayed in white */ /** Body paragraphs displayed in white */
body: string[]; body: string[];
/** Call to action section with label and URL */ /** Call to action section with label and URL (optional) */
callToAction: { callToAction?: {
label: string; label: string;
action: string; action: string;
}; };
/** Footer text displayed in gray (usage instructions) */ /** Footer text displayed in gray (usage instructions) */
footer?: string; footer?: string;
/** Level/variant for styling (default: 'warn' = yellow, 'info' = blue) */
level?: CardBoxLevel;
} }
/** /**
@@ -26,22 +33,30 @@ export interface CardBoxConfig {
* @returns Formatted string ready for console.log * @returns Formatted string ready for console.log
*/ */
export function displayCardBox(config: CardBoxConfig): string { export function displayCardBox(config: CardBoxConfig): string {
const { header, body, callToAction, footer } = config; const { header, body, callToAction, footer, level = 'warn' } = config;
// Determine colors based on level
const headerColor = level === 'info' ? chalk.blue.bold : chalk.yellow.bold;
const borderColor = level === 'info' ? 'blue' : 'yellow';
// Build the content sections // Build the content sections
const sections: string[] = [ const sections: string[] = [
// Header // Header
chalk.yellow.bold(header), headerColor(header),
// Body paragraphs // Body paragraphs
...body.map((paragraph) => chalk.white(paragraph)), ...body.map((paragraph) => chalk.white(paragraph))
// Call to action
chalk.cyan(callToAction.label) +
'\n' +
chalk.blue.underline(callToAction.action)
]; ];
// Add call to action if provided
if (callToAction && callToAction.label && callToAction.action) {
sections.push(
chalk.cyan(callToAction.label) +
'\n' +
chalk.blue.underline(callToAction.action)
);
}
// Add footer if provided // Add footer if provided
if (footer) { if (footer) {
sections.push(chalk.gray(footer)); sections.push(chalk.gray(footer));
@@ -53,7 +68,7 @@ export function displayCardBox(config: CardBoxConfig): string {
// Wrap in boxen // Wrap in boxen
return boxen(content, { return boxen(content, {
padding: 1, padding: 1,
borderColor: 'yellow', borderColor,
borderStyle: 'round', borderStyle: 'round',
margin: { top: 1, bottom: 1 } margin: { top: 1, bottom: 1 }
}); });

View File

@@ -0,0 +1,144 @@
/**
* @fileoverview Command guard for CLI commands
* CLI presentation layer - uses tm-core for logic, displays with cardBox
*/
import chalk from 'chalk';
import { createTmCore, type TmCore, type LocalOnlyCommand } from '@tm/core';
import { displayCardBox } from '../ui/components/cardBox.component.js';
/**
* Command-specific messaging configuration
*/
interface CommandMessage {
header: string;
getBody: (briefName: string) => string[];
footer: string;
}
/**
* Get command-specific message configuration
*
* NOTE: Command groups below are intentionally hardcoded (not imported from LOCAL_ONLY_COMMANDS)
* to allow flexible categorization with custom messaging per category. All commands here are
* subsets of LOCAL_ONLY_COMMANDS from @tm/core, which is the source of truth for blocked commands.
*
* Categories exist for UX purposes (tailored messaging), while LOCAL_ONLY_COMMANDS exists for
* enforcement (what's actually blocked when using Hamster).
*/
function getCommandMessage(commandName: LocalOnlyCommand): CommandMessage {
// Dependency management commands
if (
[
'add-dependency',
'remove-dependency',
'validate-dependencies',
'fix-dependencies'
].includes(commandName)
) {
return {
header: 'Hamster Manages Dependencies',
getBody: (briefName) => [
`Hamster handles dependencies for the ${chalk.blue(`"${briefName}"`)} Brief.`,
`To manage dependencies manually, log out with ${chalk.cyan('tm auth logout')} and work locally.`
],
footer:
'Switch between local and remote workflows anytime by logging in/out.'
};
}
// Subtask management commands
if (commandName === 'clear-subtasks') {
return {
header: 'Hamster Manages Subtasks',
getBody: (briefName) => [
`Hamster handles subtask management for the ${chalk.blue(`"${briefName}"`)} Brief.`,
`To manage subtasks manually, log out with ${chalk.cyan('tm auth logout')} and work locally.`
],
footer:
'Switch between local and remote workflows anytime by logging in/out.'
};
}
// Model configuration commands
if (commandName === 'models') {
return {
header: 'Hamster Manages AI Models',
getBody: (briefName) => [
`Hamster configures AI models automatically for the ${chalk.blue(`"${briefName}"`)} Brief.`,
`To configure models manually, log out with ${chalk.cyan('tm auth logout')} and work locally.`
],
footer:
'Switch between local and remote workflows anytime by logging in/out.'
};
}
// Default message for any other local-only commands
return {
header: 'Command Not Available in Hamster',
getBody: (briefName) => [
`The ${chalk.cyan(commandName)} command is managed by Hamster for the ${chalk.blue(`"${briefName}"`)} Brief.`,
`To use this command, log out with ${chalk.cyan('tm auth logout')} and work locally.`
],
footer:
'Switch between local and remote workflows anytime by logging in/out.'
};
}
/**
* Check if a command should be blocked when authenticated and display CLI message
*
* Use this for CLI commands that are only available for local file storage (not Hamster).
* This uses tm-core's AuthDomain.guardCommand() and formats the result for CLI display.
*
* @param commandName - Name of the command being executed
* @param tmCoreOrProjectRoot - TmCore instance or project root path
* @returns true if command should be blocked, false otherwise
*
* @example
* ```ts
* const isBlocked = await checkAndBlockIfAuthenticated('add-dependency', projectRoot);
* if (isBlocked) {
* process.exit(1);
* }
* ```
*/
export async function checkAndBlockIfAuthenticated(
commandName: string,
tmCoreOrProjectRoot: TmCore | string
): Promise<boolean> {
// Get or create TmCore instance
const tmCore =
typeof tmCoreOrProjectRoot === 'string'
? await createTmCore({ projectPath: tmCoreOrProjectRoot })
: tmCoreOrProjectRoot;
// Use tm-core's auth domain to check the command guard
const result = await tmCore.auth.guardCommand(
commandName,
tmCore.tasks.getStorageType()
);
if (result.isBlocked) {
// Get command-specific message configuration
// Safe to cast: guardCommand only blocks commands in LOCAL_ONLY_COMMANDS
const message = getCommandMessage(commandName as LocalOnlyCommand);
const briefName = result.briefName || 'remote brief';
// Format and display CLI message with cardBox
console.log(
displayCardBox({
header: message.header,
body: message.getBody(briefName),
footer: message.footer,
level: 'info'
})
);
return true;
}
return false;
}
// Legacy export for backward compatibility
export const checkAndBlockDependencyCommand = checkAndBlockIfAuthenticated;

View File

@@ -12,6 +12,12 @@ export {
type CheckAuthOptions type CheckAuthOptions
} from './auth-helpers.js'; } from './auth-helpers.js';
// Command guard for local-only commands
export {
checkAndBlockIfAuthenticated,
checkAndBlockDependencyCommand // Legacy export
} from './command-guard.js';
// Error handling utilities // Error handling utilities
export { displayError, isDebugMode } from './error-handler.js'; export { displayError, isDebugMode } from './error-handler.js';

View File

@@ -5,5 +5,7 @@
export * from './tools/autopilot/index.js'; export * from './tools/autopilot/index.js';
export * from './tools/tasks/index.js'; export * from './tools/tasks/index.js';
// TODO: Re-enable when TypeScript dependency tools are implemented
// export * from './tools/dependencies/index.js';
export * from './shared/utils.js'; export * from './shared/utils.js';
export * from './shared/types.js'; export * from './shared/types.js';

View File

@@ -2,6 +2,8 @@
* Shared types for MCP tools * Shared types for MCP tools
*/ */
import type { TmCore } from '@tm/core';
export interface MCPResponse<T = any> { export interface MCPResponse<T = any> {
success: boolean; success: boolean;
data?: T; data?: T;
@@ -31,6 +33,29 @@ export interface MCPContext {
session: any; session: any;
} }
/**
* Enhanced MCP context with tmCore instance
*/
export interface ToolContext {
/** Logger instance (matches fastmcp's Context.log signature) */
log: {
info: (message: string, data?: any) => void;
warn: (message: string, data?: any) => void;
error: (message: string, data?: any) => void;
debug: (message: string, data?: any) => void;
};
/** MCP session */
session?: {
roots?: Array<{ uri: string; name?: string }>;
env?: Record<string, string>;
clientCapabilities?: {
sampling?: Record<string, unknown>;
};
};
/** TmCore instance (already initialized) */
tmCore: TmCore;
}
export interface WithProjectRoot { export interface WithProjectRoot {
projectRoot: string; projectRoot: string;
} }

View File

@@ -2,10 +2,16 @@
* Shared utilities for MCP tools * Shared utilities for MCP tools
*/ */
import type { ContentResult } from 'fastmcp';
import path from 'node:path';
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path';
import {
LOCAL_ONLY_COMMANDS,
createTmCore,
type LocalOnlyCommand
} from '@tm/core';
import type { ContentResult, Context } from 'fastmcp';
import packageJson from '../../../../package.json' with { type: 'json' }; import packageJson from '../../../../package.json' with { type: 'json' };
import type { ToolContext } from './types.js';
/** /**
* Get version information * Get version information
@@ -17,6 +23,82 @@ export function getVersionInfo() {
}; };
} }
/**
* Creates a content response for MCP tools
* FastMCP requires text type, so we format objects as JSON strings
*/
export function createContentResponse(content: any): ContentResult {
return {
content: [
{
type: 'text',
text:
typeof content === 'object'
? // Format JSON nicely with indentation
JSON.stringify(content, null, 2)
: // Keep other content types as-is
String(content)
}
]
};
}
/**
* Creates an error response for MCP tools
*/
export function createErrorResponse(
errorMessage: string,
versionInfo?: { version: string; name: string },
tagInfo?: { currentTag: string }
): ContentResult {
// Provide fallback version info if not provided
if (!versionInfo) {
versionInfo = getVersionInfo();
}
let responseText = `Error: ${errorMessage}
Version: ${versionInfo.version}
Name: ${versionInfo.name}`;
// Add tag information if available
if (tagInfo) {
responseText += `
Current Tag: ${tagInfo.currentTag}`;
}
return {
content: [
{
type: 'text',
text: responseText
}
],
isError: true
};
}
/**
* Function signature for progress reporting
*/
export type ReportProgressFn = (progress: number, total?: number) => void;
/**
* Validate that reportProgress is available for long-running operations
*/
export function checkProgressCapability(
reportProgress: any,
log: any
): ReportProgressFn | undefined {
if (typeof reportProgress !== 'function') {
log?.debug?.(
'reportProgress not available - operation will run without progress updates'
);
return undefined;
}
return reportProgress;
}
/** /**
* Get current tag for a project root * Get current tag for a project root
*/ */
@@ -183,7 +265,7 @@ function getProjectRootFromSession(session: any): string | null {
export function withNormalizedProjectRoot<T extends { projectRoot?: string }>( export function withNormalizedProjectRoot<T extends { projectRoot?: string }>(
fn: ( fn: (
args: T & { projectRoot: string }, args: T & { projectRoot: string },
context: any context: Context<undefined>
) => Promise<ContentResult> ) => Promise<ContentResult>
): (args: T, context: any) => Promise<ContentResult> { ): (args: T, context: any) => Promise<ContentResult> {
return async (args: T, context: any): Promise<ContentResult> => { return async (args: T, context: any): Promise<ContentResult> => {
@@ -268,3 +350,87 @@ export function withNormalizedProjectRoot<T extends { projectRoot?: string }>(
} }
}; };
} }
/**
* Tool execution function signature with tmCore provided
*/
export type ToolExecuteFn<TArgs = any, TResult = any> = (
args: TArgs,
context: ToolContext
) => Promise<TResult>;
/**
* Higher-order function that wraps MCP tool execution with:
* - Normalized project root (via withNormalizedProjectRoot)
* - TmCore instance creation
* - Command guard check (for local-only commands)
*
* Use this for ALL MCP tools to provide consistent context and auth checking.
*
* @param commandName - Name of the command (used for guard check)
* @param executeFn - Tool execution function that receives args and enhanced context
* @returns Wrapped execute function
*
* @example
* ```ts
* export function registerAddDependencyTool(server: FastMCP) {
* server.addTool({
* name: 'add_dependency',
* parameters: AddDependencySchema,
* execute: withToolContext('add-dependency', async (args, context) => {
* // context.tmCore is already available
* // Auth guard already checked
* // Just implement the tool logic!
* })
* });
* }
* ```
*/
export function withToolContext<TArgs extends { projectRoot?: string }>(
commandName: string,
executeFn: ToolExecuteFn<TArgs & { projectRoot: string }, ContentResult>
) {
return withNormalizedProjectRoot(
async (
args: TArgs & { projectRoot: string },
context: Context<undefined>
) => {
// Create tmCore instance
const tmCore = await createTmCore({
projectPath: args.projectRoot,
loggerConfig: { mcpMode: true, logCallback: context.log }
});
// Check if this is a local-only command that needs auth guard
if (LOCAL_ONLY_COMMANDS.includes(commandName as LocalOnlyCommand)) {
const authResult = await tmCore.auth.guardCommand(
commandName,
tmCore.tasks.getStorageType()
);
if (authResult.isBlocked) {
const errorMsg = `You're working on the ${authResult.briefName} Brief in Hamster so this command is managed for you. This command is only available for local file storage. Log out with 'tm auth logout' to use local commands.`;
context.log.info(errorMsg);
return handleApiResult({
result: {
success: false,
error: { message: errorMsg }
},
log: context.log,
projectRoot: args.projectRoot
});
}
}
// Create enhanced context with tmCore
const enhancedContext: ToolContext = {
log: context.log,
session: context.session,
tmCore
};
// Execute the actual tool logic with enhanced context
return executeFn(args, enhancedContext);
}
);
}

View File

@@ -3,14 +3,11 @@
* Abort a running TDD workflow and clean up state * Abort a running TDD workflow and clean up state
*/ */
import { z } from 'zod';
import {
handleApiResult,
withNormalizedProjectRoot
} from '../../shared/utils.js';
import type { MCPContext } from '../../shared/types.js';
import { WorkflowService } from '@tm/core'; import { WorkflowService } from '@tm/core';
import type { FastMCP } from 'fastmcp'; import type { FastMCP } from 'fastmcp';
import { z } from 'zod';
import type { ToolContext } from '../../shared/types.js';
import { handleApiResult, withToolContext } from '../../shared/utils.js';
const AbortSchema = z.object({ const AbortSchema = z.object({
projectRoot: z projectRoot: z
@@ -29,12 +26,13 @@ export function registerAutopilotAbortTool(server: FastMCP) {
description: description:
'Abort the current TDD workflow and clean up workflow state. This will remove the workflow state file but will NOT delete the git branch or any code changes.', 'Abort the current TDD workflow and clean up workflow state. This will remove the workflow state file but will NOT delete the git branch or any code changes.',
parameters: AbortSchema, parameters: AbortSchema,
execute: withNormalizedProjectRoot( execute: withToolContext(
async (args: AbortArgs, context: MCPContext) => { 'autopilot-abort',
async (args: AbortArgs, { log }: ToolContext) => {
const { projectRoot } = args; const { projectRoot } = args;
try { try {
context.log.info(`Aborting autopilot workflow in ${projectRoot}`); log.info(`Aborting autopilot workflow in ${projectRoot}`);
const workflowService = new WorkflowService(projectRoot); const workflowService = new WorkflowService(projectRoot);
@@ -42,7 +40,7 @@ export function registerAutopilotAbortTool(server: FastMCP) {
const hasWorkflow = await workflowService.hasWorkflow(); const hasWorkflow = await workflowService.hasWorkflow();
if (!hasWorkflow) { if (!hasWorkflow) {
context.log.warn('No active workflow to abort'); log.warn('No active workflow to abort');
return handleApiResult({ return handleApiResult({
result: { result: {
success: true, success: true,
@@ -51,7 +49,7 @@ export function registerAutopilotAbortTool(server: FastMCP) {
hadWorkflow: false hadWorkflow: false
} }
}, },
log: context.log, log,
projectRoot projectRoot
}); });
} }
@@ -63,7 +61,7 @@ export function registerAutopilotAbortTool(server: FastMCP) {
// Abort workflow // Abort workflow
await workflowService.abortWorkflow(); await workflowService.abortWorkflow();
context.log.info('Workflow state deleted'); log.info('Workflow state deleted');
return handleApiResult({ return handleApiResult({
result: { result: {
@@ -76,20 +74,20 @@ export function registerAutopilotAbortTool(server: FastMCP) {
note: 'Git branch and code changes were preserved. You can manually clean them up if needed.' note: 'Git branch and code changes were preserved. You can manually clean them up if needed.'
} }
}, },
log: context.log, log,
projectRoot projectRoot
}); });
} catch (error: any) { } catch (error: any) {
context.log.error(`Error in autopilot-abort: ${error.message}`); log.error(`Error in autopilot-abort: ${error.message}`);
if (error.stack) { if (error.stack) {
context.log.debug(error.stack); log.debug(error.stack);
} }
return handleApiResult({ return handleApiResult({
result: { result: {
success: false, success: false,
error: { message: `Failed to abort workflow: ${error.message}` } error: { message: `Failed to abort workflow: ${error.message}` }
}, },
log: context.log, log,
projectRoot projectRoot
}); });
} }

View File

@@ -3,14 +3,11 @@
* Create a git commit with automatic staging and message generation * Create a git commit with automatic staging and message generation
*/ */
import { z } from 'zod'; import { CommitMessageGenerator, GitAdapter, WorkflowService } from '@tm/core';
import {
handleApiResult,
withNormalizedProjectRoot
} from '../../shared/utils.js';
import type { MCPContext } from '../../shared/types.js';
import { WorkflowService, GitAdapter, CommitMessageGenerator } from '@tm/core';
import type { FastMCP } from 'fastmcp'; import type { FastMCP } from 'fastmcp';
import { z } from 'zod';
import type { ToolContext } from '../../shared/types.js';
import { handleApiResult, withToolContext } from '../../shared/utils.js';
const CommitSchema = z.object({ const CommitSchema = z.object({
projectRoot: z projectRoot: z
@@ -39,12 +36,13 @@ export function registerAutopilotCommitTool(server: FastMCP) {
description: description:
'Create a git commit with automatic staging, message generation, and metadata embedding. Generates appropriate commit messages based on subtask context and TDD phase.', 'Create a git commit with automatic staging, message generation, and metadata embedding. Generates appropriate commit messages based on subtask context and TDD phase.',
parameters: CommitSchema, parameters: CommitSchema,
execute: withNormalizedProjectRoot( execute: withToolContext(
async (args: CommitArgs, context: MCPContext) => { 'autopilot-commit',
async (args: CommitArgs, { log }: ToolContext) => {
const { projectRoot, files, customMessage } = args; const { projectRoot, files, customMessage } = args;
try { try {
context.log.info(`Creating commit for workflow in ${projectRoot}`); log.info(`Creating commit for workflow in ${projectRoot}`);
const workflowService = new WorkflowService(projectRoot); const workflowService = new WorkflowService(projectRoot);
@@ -58,7 +56,7 @@ export function registerAutopilotCommitTool(server: FastMCP) {
'No active workflow found. Start a workflow with autopilot_start' 'No active workflow found. Start a workflow with autopilot_start'
} }
}, },
log: context.log, log,
projectRoot projectRoot
}); });
} }
@@ -70,9 +68,7 @@ export function registerAutopilotCommitTool(server: FastMCP) {
// Verify we're in COMMIT phase // Verify we're in COMMIT phase
if (status.tddPhase !== 'COMMIT') { if (status.tddPhase !== 'COMMIT') {
context.log.warn( log.warn(`Not in COMMIT phase (currently in ${status.tddPhase})`);
`Not in COMMIT phase (currently in ${status.tddPhase})`
);
return handleApiResult({ return handleApiResult({
result: { result: {
success: false, success: false,
@@ -80,7 +76,7 @@ export function registerAutopilotCommitTool(server: FastMCP) {
message: `Cannot commit: currently in ${status.tddPhase} phase. Complete the ${status.tddPhase} phase first using autopilot_complete_phase` message: `Cannot commit: currently in ${status.tddPhase} phase. Complete the ${status.tddPhase} phase first using autopilot_complete_phase`
} }
}, },
log: context.log, log,
projectRoot projectRoot
}); });
} }
@@ -92,7 +88,7 @@ export function registerAutopilotCommitTool(server: FastMCP) {
success: false, success: false,
error: { message: 'No active subtask to commit' } error: { message: 'No active subtask to commit' }
}, },
log: context.log, log,
projectRoot projectRoot
}); });
} }
@@ -104,19 +100,19 @@ export function registerAutopilotCommitTool(server: FastMCP) {
try { try {
if (files && files.length > 0) { if (files && files.length > 0) {
await gitAdapter.stageFiles(files); await gitAdapter.stageFiles(files);
context.log.info(`Staged ${files.length} files`); log.info(`Staged ${files.length} files`);
} else { } else {
await gitAdapter.stageFiles(['.']); await gitAdapter.stageFiles(['.']);
context.log.info('Staged all changes'); log.info('Staged all changes');
} }
} catch (error: any) { } catch (error: any) {
context.log.error(`Failed to stage files: ${error.message}`); log.error(`Failed to stage files: ${error.message}`);
return handleApiResult({ return handleApiResult({
result: { result: {
success: false, success: false,
error: { message: `Failed to stage files: ${error.message}` } error: { message: `Failed to stage files: ${error.message}` }
}, },
log: context.log, log,
projectRoot projectRoot
}); });
} }
@@ -124,7 +120,7 @@ export function registerAutopilotCommitTool(server: FastMCP) {
// Check if there are staged changes // Check if there are staged changes
const hasStagedChanges = await gitAdapter.hasStagedChanges(); const hasStagedChanges = await gitAdapter.hasStagedChanges();
if (!hasStagedChanges) { if (!hasStagedChanges) {
context.log.warn('No staged changes to commit'); log.warn('No staged changes to commit');
return handleApiResult({ return handleApiResult({
result: { result: {
success: false, success: false,
@@ -133,7 +129,7 @@ export function registerAutopilotCommitTool(server: FastMCP) {
'No staged changes to commit. Make code changes before committing' 'No staged changes to commit. Make code changes before committing'
} }
}, },
log: context.log, log,
projectRoot projectRoot
}); });
} }
@@ -145,7 +141,7 @@ export function registerAutopilotCommitTool(server: FastMCP) {
let commitMessage: string; let commitMessage: string;
if (customMessage) { if (customMessage) {
commitMessage = customMessage; commitMessage = customMessage;
context.log.info('Using custom commit message'); log.info('Using custom commit message');
} else { } else {
const messageGenerator = new CommitMessageGenerator(); const messageGenerator = new CommitMessageGenerator();
@@ -168,21 +164,21 @@ export function registerAutopilotCommitTool(server: FastMCP) {
}; };
commitMessage = messageGenerator.generateMessage(options); commitMessage = messageGenerator.generateMessage(options);
context.log.info('Generated commit message automatically'); log.info('Generated commit message automatically');
} }
// Create commit // Create commit
try { try {
await gitAdapter.createCommit(commitMessage); await gitAdapter.createCommit(commitMessage);
context.log.info('Commit created successfully'); log.info('Commit created successfully');
} catch (error: any) { } catch (error: any) {
context.log.error(`Failed to create commit: ${error.message}`); log.error(`Failed to create commit: ${error.message}`);
return handleApiResult({ return handleApiResult({
result: { result: {
success: false, success: false,
error: { message: `Failed to create commit: ${error.message}` } error: { message: `Failed to create commit: ${error.message}` }
}, },
log: context.log, log,
projectRoot projectRoot
}); });
} }
@@ -193,7 +189,7 @@ export function registerAutopilotCommitTool(server: FastMCP) {
// Complete COMMIT phase and advance workflow // Complete COMMIT phase and advance workflow
const newStatus = await workflowService.commit(); const newStatus = await workflowService.commit();
context.log.info( log.info(
`Commit completed. Current phase: ${newStatus.tddPhase || newStatus.phase}` `Commit completed. Current phase: ${newStatus.tddPhase || newStatus.phase}`
); );
@@ -217,20 +213,20 @@ export function registerAutopilotCommitTool(server: FastMCP) {
nextSteps: nextAction.nextSteps nextSteps: nextAction.nextSteps
} }
}, },
log: context.log, log,
projectRoot projectRoot
}); });
} catch (error: any) { } catch (error: any) {
context.log.error(`Error in autopilot-commit: ${error.message}`); log.error(`Error in autopilot-commit: ${error.message}`);
if (error.stack) { if (error.stack) {
context.log.debug(error.stack); log.debug(error.stack);
} }
return handleApiResult({ return handleApiResult({
result: { result: {
success: false, success: false,
error: { message: `Failed to commit: ${error.message}` } error: { message: `Failed to commit: ${error.message}` }
}, },
log: context.log, log,
projectRoot projectRoot
}); });
} }

View File

@@ -3,14 +3,11 @@
* Complete the current TDD phase with test result validation * Complete the current TDD phase with test result validation
*/ */
import { z } from 'zod';
import {
handleApiResult,
withNormalizedProjectRoot
} from '../../shared/utils.js';
import type { MCPContext } from '../../shared/types.js';
import { WorkflowService } from '@tm/core'; import { WorkflowService } from '@tm/core';
import type { FastMCP } from 'fastmcp'; import type { FastMCP } from 'fastmcp';
import { z } from 'zod';
import type { ToolContext } from '../../shared/types.js';
import { handleApiResult, withToolContext } from '../../shared/utils.js';
const CompletePhaseSchema = z.object({ const CompletePhaseSchema = z.object({
projectRoot: z projectRoot: z
@@ -37,14 +34,13 @@ export function registerAutopilotCompleteTool(server: FastMCP) {
description: description:
'Complete the current TDD phase (RED, GREEN, or COMMIT) with test result validation. RED phase: expects failures (if 0 failures, feature is already implemented and subtask auto-completes). GREEN phase: expects all tests passing.', 'Complete the current TDD phase (RED, GREEN, or COMMIT) with test result validation. RED phase: expects failures (if 0 failures, feature is already implemented and subtask auto-completes). GREEN phase: expects all tests passing.',
parameters: CompletePhaseSchema, parameters: CompletePhaseSchema,
execute: withNormalizedProjectRoot( execute: withToolContext(
async (args: CompletePhaseArgs, context: MCPContext) => { 'autopilot-complete-phase',
async (args: CompletePhaseArgs, { log }: ToolContext) => {
const { projectRoot, testResults } = args; const { projectRoot, testResults } = args;
try { try {
context.log.info( log.info(`Completing current phase in workflow for ${projectRoot}`);
`Completing current phase in workflow for ${projectRoot}`
);
const workflowService = new WorkflowService(projectRoot); const workflowService = new WorkflowService(projectRoot);
@@ -58,7 +54,7 @@ export function registerAutopilotCompleteTool(server: FastMCP) {
'No active workflow found. Start a workflow with autopilot_start' 'No active workflow found. Start a workflow with autopilot_start'
} }
}, },
log: context.log, log,
projectRoot projectRoot
}); });
} }
@@ -76,7 +72,7 @@ export function registerAutopilotCompleteTool(server: FastMCP) {
message: `Cannot complete phase: not in a TDD phase (current phase: ${currentStatus.phase})` message: `Cannot complete phase: not in a TDD phase (current phase: ${currentStatus.phase})`
} }
}, },
log: context.log, log,
projectRoot projectRoot
}); });
} }
@@ -91,7 +87,7 @@ export function registerAutopilotCompleteTool(server: FastMCP) {
'Cannot complete COMMIT phase with this tool. Use autopilot_commit instead' 'Cannot complete COMMIT phase with this tool. Use autopilot_commit instead'
} }
}, },
log: context.log, log,
projectRoot projectRoot
}); });
} }
@@ -112,7 +108,7 @@ export function registerAutopilotCompleteTool(server: FastMCP) {
const status = await workflowService.completePhase(fullTestResults); const status = await workflowService.completePhase(fullTestResults);
const nextAction = workflowService.getNextAction(); const nextAction = workflowService.getNextAction();
context.log.info( log.info(
`Phase completed. New phase: ${status.tddPhase || status.phase}` `Phase completed. New phase: ${status.tddPhase || status.phase}`
); );
@@ -127,13 +123,13 @@ export function registerAutopilotCompleteTool(server: FastMCP) {
nextSteps: nextAction.nextSteps nextSteps: nextAction.nextSteps
} }
}, },
log: context.log, log,
projectRoot projectRoot
}); });
} catch (error: any) { } catch (error: any) {
context.log.error(`Error in autopilot-complete: ${error.message}`); log.error(`Error in autopilot-complete: ${error.message}`);
if (error.stack) { if (error.stack) {
context.log.debug(error.stack); log.debug(error.stack);
} }
return handleApiResult({ return handleApiResult({
result: { result: {
@@ -142,7 +138,7 @@ export function registerAutopilotCompleteTool(server: FastMCP) {
message: `Failed to complete phase: ${error.message}` message: `Failed to complete phase: ${error.message}`
} }
}, },
log: context.log, log,
projectRoot projectRoot
}); });
} }

View File

@@ -3,14 +3,11 @@
* Finalize and complete the workflow with working tree validation * Finalize and complete the workflow with working tree validation
*/ */
import { z } from 'zod';
import {
handleApiResult,
withNormalizedProjectRoot
} from '../../shared/utils.js';
import type { MCPContext } from '../../shared/types.js';
import { WorkflowService } from '@tm/core'; import { WorkflowService } from '@tm/core';
import type { FastMCP } from 'fastmcp'; import type { FastMCP } from 'fastmcp';
import { z } from 'zod';
import type { ToolContext } from '../../shared/types.js';
import { handleApiResult, withToolContext } from '../../shared/utils.js';
const FinalizeSchema = z.object({ const FinalizeSchema = z.object({
projectRoot: z projectRoot: z
@@ -29,12 +26,13 @@ export function registerAutopilotFinalizeTool(server: FastMCP) {
description: description:
'Finalize and complete the workflow. Validates that all changes are committed and working tree is clean before marking workflow as complete.', 'Finalize and complete the workflow. Validates that all changes are committed and working tree is clean before marking workflow as complete.',
parameters: FinalizeSchema, parameters: FinalizeSchema,
execute: withNormalizedProjectRoot( execute: withToolContext(
async (args: FinalizeArgs, context: MCPContext) => { 'autopilot-finalize',
async (args: FinalizeArgs, { log }: ToolContext) => {
const { projectRoot } = args; const { projectRoot } = args;
try { try {
context.log.info(`Finalizing workflow in ${projectRoot}`); log.info(`Finalizing workflow in ${projectRoot}`);
const workflowService = new WorkflowService(projectRoot); const workflowService = new WorkflowService(projectRoot);
@@ -48,7 +46,7 @@ export function registerAutopilotFinalizeTool(server: FastMCP) {
'No active workflow found. Start a workflow with autopilot_start' 'No active workflow found. Start a workflow with autopilot_start'
} }
}, },
log: context.log, log,
projectRoot projectRoot
}); });
} }
@@ -66,7 +64,7 @@ export function registerAutopilotFinalizeTool(server: FastMCP) {
message: `Cannot finalize: workflow is in ${currentStatus.phase} phase. Complete all subtasks first.` message: `Cannot finalize: workflow is in ${currentStatus.phase} phase. Complete all subtasks first.`
} }
}, },
log: context.log, log,
projectRoot projectRoot
}); });
} }
@@ -74,7 +72,7 @@ export function registerAutopilotFinalizeTool(server: FastMCP) {
// Finalize workflow (validates clean working tree) // Finalize workflow (validates clean working tree)
const newStatus = await workflowService.finalizeWorkflow(); const newStatus = await workflowService.finalizeWorkflow();
context.log.info('Workflow finalized successfully'); log.info('Workflow finalized successfully');
// Get next action // Get next action
const nextAction = workflowService.getNextAction(); const nextAction = workflowService.getNextAction();
@@ -89,13 +87,13 @@ export function registerAutopilotFinalizeTool(server: FastMCP) {
nextSteps: nextAction.nextSteps nextSteps: nextAction.nextSteps
} }
}, },
log: context.log, log,
projectRoot projectRoot
}); });
} catch (error: any) { } catch (error: any) {
context.log.error(`Error in autopilot-finalize: ${error.message}`); log.error(`Error in autopilot-finalize: ${error.message}`);
if (error.stack) { if (error.stack) {
context.log.debug(error.stack); log.debug(error.stack);
} }
return handleApiResult({ return handleApiResult({
result: { result: {
@@ -104,7 +102,7 @@ export function registerAutopilotFinalizeTool(server: FastMCP) {
message: `Failed to finalize workflow: ${error.message}` message: `Failed to finalize workflow: ${error.message}`
} }
}, },
log: context.log, log,
projectRoot projectRoot
}); });
} }

View File

@@ -3,14 +3,11 @@
* Get the next action to perform in the TDD workflow * Get the next action to perform in the TDD workflow
*/ */
import { z } from 'zod';
import {
handleApiResult,
withNormalizedProjectRoot
} from '../../shared/utils.js';
import type { MCPContext } from '../../shared/types.js';
import { WorkflowService } from '@tm/core'; import { WorkflowService } from '@tm/core';
import type { FastMCP } from 'fastmcp'; import type { FastMCP } from 'fastmcp';
import { z } from 'zod';
import type { ToolContext } from '../../shared/types.js';
import { handleApiResult, withToolContext } from '../../shared/utils.js';
const NextActionSchema = z.object({ const NextActionSchema = z.object({
projectRoot: z projectRoot: z
@@ -29,14 +26,13 @@ export function registerAutopilotNextTool(server: FastMCP) {
description: description:
'Get the next action to perform in the TDD workflow. Returns detailed context about what needs to be done next, including the current phase, subtask, and expected actions.', 'Get the next action to perform in the TDD workflow. Returns detailed context about what needs to be done next, including the current phase, subtask, and expected actions.',
parameters: NextActionSchema, parameters: NextActionSchema,
execute: withNormalizedProjectRoot( execute: withToolContext(
async (args: NextActionArgs, context: MCPContext) => { 'autopilot-next',
async (args: NextActionArgs, { log }: ToolContext) => {
const { projectRoot } = args; const { projectRoot } = args;
try { try {
context.log.info( log.info(`Getting next action for workflow in ${projectRoot}`);
`Getting next action for workflow in ${projectRoot}`
);
const workflowService = new WorkflowService(projectRoot); const workflowService = new WorkflowService(projectRoot);
@@ -50,7 +46,7 @@ export function registerAutopilotNextTool(server: FastMCP) {
'No active workflow found. Start a workflow with autopilot_start' 'No active workflow found. Start a workflow with autopilot_start'
} }
}, },
log: context.log, log,
projectRoot projectRoot
}); });
} }
@@ -62,7 +58,7 @@ export function registerAutopilotNextTool(server: FastMCP) {
const nextAction = workflowService.getNextAction(); const nextAction = workflowService.getNextAction();
const status = workflowService.getStatus(); const status = workflowService.getStatus();
context.log.info(`Next action determined: ${nextAction.action}`); log.info(`Next action determined: ${nextAction.action}`);
return handleApiResult({ return handleApiResult({
result: { result: {
@@ -74,13 +70,13 @@ export function registerAutopilotNextTool(server: FastMCP) {
nextSteps: nextAction.nextSteps nextSteps: nextAction.nextSteps
} }
}, },
log: context.log, log,
projectRoot projectRoot
}); });
} catch (error: any) { } catch (error: any) {
context.log.error(`Error in autopilot-next: ${error.message}`); log.error(`Error in autopilot-next: ${error.message}`);
if (error.stack) { if (error.stack) {
context.log.debug(error.stack); log.debug(error.stack);
} }
return handleApiResult({ return handleApiResult({
result: { result: {
@@ -89,7 +85,7 @@ export function registerAutopilotNextTool(server: FastMCP) {
message: `Failed to get next action: ${error.message}` message: `Failed to get next action: ${error.message}`
} }
}, },
log: context.log, log,
projectRoot projectRoot
}); });
} }

View File

@@ -4,11 +4,8 @@
*/ */
import { z } from 'zod'; import { z } from 'zod';
import { import { handleApiResult, withToolContext } from '../../shared/utils.js';
handleApiResult, import type { ToolContext } from '../../shared/types.js';
withNormalizedProjectRoot
} from '../../shared/utils.js';
import type { MCPContext } from '../../shared/types.js';
import { WorkflowService } from '@tm/core'; import { WorkflowService } from '@tm/core';
import type { FastMCP } from 'fastmcp'; import type { FastMCP } from 'fastmcp';
@@ -29,12 +26,13 @@ export function registerAutopilotResumeTool(server: FastMCP) {
description: description:
'Resume a previously started TDD workflow from saved state. Restores the workflow state machine and continues from where it left off.', 'Resume a previously started TDD workflow from saved state. Restores the workflow state machine and continues from where it left off.',
parameters: ResumeWorkflowSchema, parameters: ResumeWorkflowSchema,
execute: withNormalizedProjectRoot( execute: withToolContext(
async (args: ResumeWorkflowArgs, context: MCPContext) => { 'autopilot-resume',
async (args: ResumeWorkflowArgs, { log }: ToolContext) => {
const { projectRoot } = args; const { projectRoot } = args;
try { try {
context.log.info(`Resuming autopilot workflow in ${projectRoot}`); log.info(`Resuming autopilot workflow in ${projectRoot}`);
const workflowService = new WorkflowService(projectRoot); const workflowService = new WorkflowService(projectRoot);
@@ -48,7 +46,7 @@ export function registerAutopilotResumeTool(server: FastMCP) {
'No workflow state found. Start a new workflow with autopilot_start' 'No workflow state found. Start a new workflow with autopilot_start'
} }
}, },
log: context.log, log,
projectRoot projectRoot
}); });
} }
@@ -57,9 +55,7 @@ export function registerAutopilotResumeTool(server: FastMCP) {
const status = await workflowService.resumeWorkflow(); const status = await workflowService.resumeWorkflow();
const nextAction = workflowService.getNextAction(); const nextAction = workflowService.getNextAction();
context.log.info( log.info(`Workflow resumed successfully for task ${status.taskId}`);
`Workflow resumed successfully for task ${status.taskId}`
);
return handleApiResult({ return handleApiResult({
result: { result: {
@@ -72,20 +68,20 @@ export function registerAutopilotResumeTool(server: FastMCP) {
nextSteps: nextAction.nextSteps nextSteps: nextAction.nextSteps
} }
}, },
log: context.log, log,
projectRoot projectRoot
}); });
} catch (error: any) { } catch (error: any) {
context.log.error(`Error in autopilot-resume: ${error.message}`); log.error(`Error in autopilot-resume: ${error.message}`);
if (error.stack) { if (error.stack) {
context.log.debug(error.stack); log.debug(error.stack);
} }
return handleApiResult({ return handleApiResult({
result: { result: {
success: false, success: false,
error: { message: `Failed to resume workflow: ${error.message}` } error: { message: `Failed to resume workflow: ${error.message}` }
}, },
log: context.log, log,
projectRoot projectRoot
}); });
} }

View File

@@ -4,12 +4,8 @@
*/ */
import { z } from 'zod'; import { z } from 'zod';
import { import { handleApiResult, withToolContext } from '../../shared/utils.js';
handleApiResult, import type { ToolContext } from '../../shared/types.js';
withNormalizedProjectRoot
} from '../../shared/utils.js';
import type { MCPContext } from '../../shared/types.js';
import { createTmCore } from '@tm/core';
import { WorkflowService } from '@tm/core'; import { WorkflowService } from '@tm/core';
import type { FastMCP } from 'fastmcp'; import type { FastMCP } from 'fastmcp';
@@ -57,12 +53,13 @@ export function registerAutopilotStartTool(server: FastMCP) {
description: description:
'Initialize and start a new TDD workflow for a task. Creates a git branch and sets up the workflow state machine.', 'Initialize and start a new TDD workflow for a task. Creates a git branch and sets up the workflow state machine.',
parameters: StartWorkflowSchema, parameters: StartWorkflowSchema,
execute: withNormalizedProjectRoot( execute: withToolContext(
async (args: StartWorkflowArgs, context: MCPContext) => { 'autopilot-start',
async (args: StartWorkflowArgs, { log, tmCore }: ToolContext) => {
const { taskId, projectRoot, maxAttempts, force } = args; const { taskId, projectRoot, maxAttempts, force } = args;
try { try {
context.log.info( log.info(
`Starting autopilot workflow for task ${taskId} in ${projectRoot}` `Starting autopilot workflow for task ${taskId} in ${projectRoot}`
); );
@@ -75,20 +72,15 @@ export function registerAutopilotStartTool(server: FastMCP) {
message: `Task ID "${taskId}" is a subtask. Autopilot workflows can only be started for main tasks (e.g., "1", "2", "HAM-123"). Please provide the parent task ID instead.` message: `Task ID "${taskId}" is a subtask. Autopilot workflows can only be started for main tasks (e.g., "1", "2", "HAM-123"). Please provide the parent task ID instead.`
} }
}, },
log: context.log, log,
projectRoot projectRoot
}); });
} }
// Load task data and get current tag
const core = await createTmCore({
projectPath: projectRoot
});
// Get current tag from ConfigManager // Get current tag from ConfigManager
const currentTag = core.config.getActiveTag(); const currentTag = tmCore.config.getActiveTag();
const taskResult = await core.tasks.get(taskId); const taskResult = await tmCore.tasks.get(taskId);
if (!taskResult || !taskResult.task) { if (!taskResult || !taskResult.task) {
return handleApiResult({ return handleApiResult({
@@ -96,7 +88,7 @@ export function registerAutopilotStartTool(server: FastMCP) {
success: false, success: false,
error: { message: `Task ${taskId} not found` } error: { message: `Task ${taskId} not found` }
}, },
log: context.log, log,
projectRoot projectRoot
}); });
} }
@@ -112,7 +104,7 @@ export function registerAutopilotStartTool(server: FastMCP) {
message: `Task ${taskId} has no subtasks. Please use expand_task (with id="${taskId}") to create subtasks first. For improved results, consider running analyze_complexity before expanding the task.` message: `Task ${taskId} has no subtasks. Please use expand_task (with id="${taskId}") to create subtasks first. For improved results, consider running analyze_complexity before expanding the task.`
} }
}, },
log: context.log, log,
projectRoot projectRoot
}); });
} }
@@ -123,7 +115,7 @@ export function registerAutopilotStartTool(server: FastMCP) {
// Check for existing workflow // Check for existing workflow
const hasWorkflow = await workflowService.hasWorkflow(); const hasWorkflow = await workflowService.hasWorkflow();
if (hasWorkflow && !force) { if (hasWorkflow && !force) {
context.log.warn('Workflow state already exists'); log.warn('Workflow state already exists');
return handleApiResult({ return handleApiResult({
result: { result: {
success: false, success: false,
@@ -132,7 +124,7 @@ export function registerAutopilotStartTool(server: FastMCP) {
'Workflow already in progress. Use force=true to override or resume the existing workflow. Suggestion: Use autopilot_resume to continue the existing workflow' 'Workflow already in progress. Use force=true to override or resume the existing workflow. Suggestion: Use autopilot_resume to continue the existing workflow'
} }
}, },
log: context.log, log,
projectRoot projectRoot
}); });
} }
@@ -152,7 +144,7 @@ export function registerAutopilotStartTool(server: FastMCP) {
tag: currentTag // Pass current tag for branch naming tag: currentTag // Pass current tag for branch naming
}); });
context.log.info(`Workflow started successfully for task ${taskId}`); log.info(`Workflow started successfully for task ${taskId}`);
// Get next action with guidance from WorkflowService // Get next action with guidance from WorkflowService
const nextAction = workflowService.getNextAction(); const nextAction = workflowService.getNextAction();
@@ -172,20 +164,20 @@ export function registerAutopilotStartTool(server: FastMCP) {
nextSteps: nextAction.nextSteps nextSteps: nextAction.nextSteps
} }
}, },
log: context.log, log,
projectRoot projectRoot
}); });
} catch (error: any) { } catch (error: any) {
context.log.error(`Error in autopilot-start: ${error.message}`); log.error(`Error in autopilot-start: ${error.message}`);
if (error.stack) { if (error.stack) {
context.log.debug(error.stack); log.debug(error.stack);
} }
return handleApiResult({ return handleApiResult({
result: { result: {
success: false, success: false,
error: { message: `Failed to start workflow: ${error.message}` } error: { message: `Failed to start workflow: ${error.message}` }
}, },
log: context.log, log,
projectRoot projectRoot
}); });
} }

View File

@@ -4,11 +4,8 @@
*/ */
import { z } from 'zod'; import { z } from 'zod';
import { import { handleApiResult, withToolContext } from '../../shared/utils.js';
handleApiResult, import type { ToolContext } from '../../shared/types.js';
withNormalizedProjectRoot
} from '../../shared/utils.js';
import type { MCPContext } from '../../shared/types.js';
import { WorkflowService } from '@tm/core'; import { WorkflowService } from '@tm/core';
import type { FastMCP } from 'fastmcp'; import type { FastMCP } from 'fastmcp';
@@ -29,12 +26,13 @@ export function registerAutopilotStatusTool(server: FastMCP) {
description: description:
'Get comprehensive workflow status including current phase, progress, subtask details, and activity history.', 'Get comprehensive workflow status including current phase, progress, subtask details, and activity history.',
parameters: StatusSchema, parameters: StatusSchema,
execute: withNormalizedProjectRoot( execute: withToolContext(
async (args: StatusArgs, context: MCPContext) => { 'autopilot-status',
async (args: StatusArgs, { log }: ToolContext) => {
const { projectRoot } = args; const { projectRoot } = args;
try { try {
context.log.info(`Getting workflow status for ${projectRoot}`); log.info(`Getting workflow status for ${projectRoot}`);
const workflowService = new WorkflowService(projectRoot); const workflowService = new WorkflowService(projectRoot);
@@ -48,7 +46,7 @@ export function registerAutopilotStatusTool(server: FastMCP) {
'No active workflow found. Start a workflow with autopilot_start' 'No active workflow found. Start a workflow with autopilot_start'
} }
}, },
log: context.log, log,
projectRoot projectRoot
}); });
} }
@@ -59,22 +57,20 @@ export function registerAutopilotStatusTool(server: FastMCP) {
// Get status // Get status
const status = workflowService.getStatus(); const status = workflowService.getStatus();
context.log.info( log.info(`Workflow status retrieved for task ${status.taskId}`);
`Workflow status retrieved for task ${status.taskId}`
);
return handleApiResult({ return handleApiResult({
result: { result: {
success: true, success: true,
data: status data: status
}, },
log: context.log, log,
projectRoot projectRoot
}); });
} catch (error: any) { } catch (error: any) {
context.log.error(`Error in autopilot-status: ${error.message}`); log.error(`Error in autopilot-status: ${error.message}`);
if (error.stack) { if (error.stack) {
context.log.debug(error.stack); log.debug(error.stack);
} }
return handleApiResult({ return handleApiResult({
result: { result: {
@@ -83,7 +79,7 @@ export function registerAutopilotStatusTool(server: FastMCP) {
message: `Failed to get workflow status: ${error.message}` message: `Failed to get workflow status: ${error.message}`
} }
}, },
log: context.log, log,
projectRoot projectRoot
}); });
} }

View File

@@ -6,10 +6,10 @@
import { z } from 'zod'; import { z } from 'zod';
import { import {
handleApiResult, handleApiResult,
withNormalizedProjectRoot withToolContext
} from '../../shared/utils.js'; } from '../../shared/utils.js';
import type { MCPContext } from '../../shared/types.js'; import type { ToolContext } from '../../shared/types.js';
import { createTmCore, Subtask, type Task } from '@tm/core'; import { Subtask, type Task } from '@tm/core';
import type { FastMCP } from 'fastmcp'; import type { FastMCP } from 'fastmcp';
const GetTaskSchema = z.object({ const GetTaskSchema = z.object({
@@ -40,24 +40,16 @@ export function registerGetTaskTool(server: FastMCP) {
name: 'get_task', name: 'get_task',
description: 'Get detailed information about a specific task', description: 'Get detailed information about a specific task',
parameters: GetTaskSchema, parameters: GetTaskSchema,
execute: withNormalizedProjectRoot( execute: withToolContext(
async (args: GetTaskArgs, context: MCPContext) => { 'get-task',
async (args: GetTaskArgs, { log, tmCore }: ToolContext) => {
const { id, status, projectRoot, tag } = args; const { id, status, projectRoot, tag } = args;
try { try {
context.log.info( log.info(
`Getting task details for ID: ${id}${status ? ` (filtering subtasks by status: ${status})` : ''} in root: ${projectRoot}` `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 // Handle comma-separated IDs - parallelize for better performance
const taskIds = id.split(',').map((tid) => tid.trim()); const taskIds = id.split(',').map((tid) => tid.trim());
const results = await Promise.all( const results = await Promise.all(
@@ -83,7 +75,7 @@ export function registerGetTaskTool(server: FastMCP) {
} }
if (tasks.length === 0) { if (tasks.length === 0) {
context.log.warn(`No tasks found for ID(s): ${id}`); log.warn(`No tasks found for ID(s): ${id}`);
return handleApiResult({ return handleApiResult({
result: { result: {
success: false, success: false,
@@ -91,12 +83,12 @@ export function registerGetTaskTool(server: FastMCP) {
message: `No tasks found for ID(s): ${id}` message: `No tasks found for ID(s): ${id}`
} }
}, },
log: context.log, log,
projectRoot projectRoot
}); });
} }
context.log.info( log.info(
`Successfully retrieved ${tasks.length} task(s) for ID(s): ${id}` `Successfully retrieved ${tasks.length} task(s) for ID(s): ${id}`
); );
@@ -108,14 +100,14 @@ export function registerGetTaskTool(server: FastMCP) {
success: true, success: true,
data: responseData data: responseData
}, },
log: context.log, log,
projectRoot, projectRoot,
tag tag
}); });
} catch (error: any) { } catch (error: any) {
context.log.error(`Error in get-task: ${error.message}`); log.error(`Error in get-task: ${error.message}`);
if (error.stack) { if (error.stack) {
context.log.debug(error.stack); log.debug(error.stack);
} }
return handleApiResult({ return handleApiResult({
result: { result: {
@@ -124,7 +116,7 @@ export function registerGetTaskTool(server: FastMCP) {
message: `Failed to get task: ${error.message}` message: `Failed to get task: ${error.message}`
} }
}, },
log: context.log, log,
projectRoot projectRoot
}); });
} }

View File

@@ -6,10 +6,10 @@
import { z } from 'zod'; import { z } from 'zod';
import { import {
handleApiResult, handleApiResult,
withNormalizedProjectRoot withToolContext
} from '../../shared/utils.js'; } from '../../shared/utils.js';
import type { MCPContext } from '../../shared/types.js'; import type { ToolContext } from '../../shared/types.js';
import { createTmCore, type TaskStatus, type Task } from '@tm/core'; import type { TaskStatus, Task } from '@tm/core';
import type { FastMCP } from 'fastmcp'; import type { FastMCP } from 'fastmcp';
const GetTasksSchema = z.object({ const GetTasksSchema = z.object({
@@ -40,24 +40,16 @@ export function registerGetTasksTool(server: FastMCP) {
description: description:
'Get all tasks from Task Master, optionally filtering by status and including subtasks.', 'Get all tasks from Task Master, optionally filtering by status and including subtasks.',
parameters: GetTasksSchema, parameters: GetTasksSchema,
execute: withNormalizedProjectRoot( execute: withToolContext(
async (args: GetTasksArgs, context: MCPContext) => { 'get-tasks',
async (args: GetTasksArgs, { log, tmCore }: ToolContext) => {
const { projectRoot, status, withSubtasks, tag } = args; const { projectRoot, status, withSubtasks, tag } = args;
try { try {
context.log.info( log.info(
`Getting tasks from ${projectRoot}${status ? ` with status filter: ${status}` : ''}${tag ? ` for tag: ${tag}` : ''}` `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 // Build filter
const filter = const filter =
status && status !== 'all' status && status !== 'all'
@@ -75,7 +67,7 @@ export function registerGetTasksTool(server: FastMCP) {
includeSubtasks: withSubtasks includeSubtasks: withSubtasks
}); });
context.log.info( log.info(
`Retrieved ${result.tasks?.length || 0} tasks (${result.filtered} filtered, ${result.total} total)` `Retrieved ${result.tasks?.length || 0} tasks (${result.filtered} filtered, ${result.total} total)`
); );
@@ -138,14 +130,14 @@ export function registerGetTasksTool(server: FastMCP) {
} }
} }
}, },
log: context.log, log,
projectRoot, projectRoot,
tag: result.tag tag: result.tag
}); });
} catch (error: any) { } catch (error: any) {
context.log.error(`Error in get-tasks: ${error.message}`); log.error(`Error in get-tasks: ${error.message}`);
if (error.stack) { if (error.stack) {
context.log.debug(error.stack); log.debug(error.stack);
} }
return handleApiResult({ return handleApiResult({
result: { result: {
@@ -154,7 +146,7 @@ export function registerGetTasksTool(server: FastMCP) {
message: `Failed to get tasks: ${error.message}` message: `Failed to get tasks: ${error.message}`
} }
}, },
log: context.log, log,
projectRoot projectRoot
}); });
} }

View File

@@ -3,15 +3,11 @@
* Tool for adding a dependency to a task * Tool for adding a dependency to a task
*/ */
import { createErrorResponse, handleApiResult, withToolContext } from '@tm/mcp';
import { z } from 'zod'; import { z } from 'zod';
import { import { resolveTag } from '../../../scripts/modules/utils.js';
handleApiResult,
createErrorResponse,
withNormalizedProjectRoot
} from './utils.js';
import { addDependencyDirect } from '../core/task-master-core.js'; import { addDependencyDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js'; import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';
/** /**
* Register the addDependency tool with the MCP server * Register the addDependency tool with the MCP server
@@ -37,62 +33,65 @@ export function registerAddDependencyTool(server) {
.describe('The directory of the project. Must be an absolute path.'), .describe('The directory of the project. Must be an absolute path.'),
tag: z.string().optional().describe('Tag context to operate on') tag: z.string().optional().describe('Tag context to operate on')
}), }),
execute: withNormalizedProjectRoot(async (args, { log, session }) => { execute: withToolContext(
try { 'add-dependency',
log.info( async (args, { log, session }) => {
`Adding dependency for task ${args.id} to depend on ${args.dependsOn}`
);
const resolvedTag = resolveTag({
projectRoot: args.projectRoot,
tag: args.tag
});
let tasksJsonPath;
try { try {
tasksJsonPath = findTasksPath( log.info(
{ projectRoot: args.projectRoot, file: args.file }, `Adding dependency for task ${args.id} to depend on ${args.dependsOn}`
log
); );
} catch (error) { const resolvedTag = resolveTag({
log.error(`Error finding tasks.json: ${error.message}`); projectRoot: args.projectRoot,
return createErrorResponse( tag: args.tag
`Failed to find tasks.json: ${error.message}` });
); let tasksJsonPath;
} try {
tasksJsonPath = findTasksPath(
{ projectRoot: args.projectRoot, file: args.file },
log
);
} catch (error) {
log.error(`Error finding tasks.json: ${error.message}`);
return createErrorResponse(
`Failed to find tasks.json: ${error.message}`
);
}
// Call the direct function with the resolved path // Call the direct function with the resolved path
const result = await addDependencyDirect( const result = await addDependencyDirect(
{ {
// Pass the explicitly resolved path // Pass the explicitly resolved path
tasksJsonPath: tasksJsonPath, tasksJsonPath: tasksJsonPath,
// Pass other relevant args // Pass other relevant args
id: args.id, id: args.id,
dependsOn: args.dependsOn, dependsOn: args.dependsOn,
projectRoot: args.projectRoot,
tag: resolvedTag
},
log
// Remove context object
);
// Log result
if (result.success) {
log.info(`Successfully added dependency: ${result.data.message}`);
} else {
log.error(`Failed to add dependency: ${result.error.message}`);
}
// Use handleApiResult to format the response
return handleApiResult({
result,
log,
errorPrefix: 'Error adding dependency',
projectRoot: args.projectRoot, projectRoot: args.projectRoot,
tag: resolvedTag tag: resolvedTag
}, });
log } catch (error) {
// Remove context object log.error(`Error in addDependency tool: ${error.message}`);
); return createErrorResponse(error.message);
// Log result
if (result.success) {
log.info(`Successfully added dependency: ${result.data.message}`);
} else {
log.error(`Failed to add dependency: ${result.error.message}`);
} }
// Use handleApiResult to format the response
return handleApiResult(
result,
log,
'Error adding dependency',
undefined,
args.projectRoot
);
} catch (error) {
log.error(`Error in addDependency tool: ${error.message}`);
return createErrorResponse(error.message);
} }
}) )
}); });
} }

View File

@@ -8,7 +8,7 @@ import {
handleApiResult, handleApiResult,
createErrorResponse, createErrorResponse,
withNormalizedProjectRoot withNormalizedProjectRoot
} from './utils.js'; } from '@tm/mcp';
import { addSubtaskDirect } from '../core/task-master-core.js'; import { addSubtaskDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js'; import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js'; import { resolveTag } from '../../../scripts/modules/utils.js';

View File

@@ -8,7 +8,7 @@ import {
createErrorResponse, createErrorResponse,
handleApiResult, handleApiResult,
withNormalizedProjectRoot withNormalizedProjectRoot
} from './utils.js'; } from '@tm/mcp';
import { addTagDirect } from '../core/task-master-core.js'; import { addTagDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js'; import { findTasksPath } from '../core/utils/path-utils.js';

View File

@@ -8,7 +8,7 @@ import {
createErrorResponse, createErrorResponse,
handleApiResult, handleApiResult,
withNormalizedProjectRoot withNormalizedProjectRoot
} from './utils.js'; } from '@tm/mcp';
import { addTaskDirect } from '../core/task-master-core.js'; import { addTaskDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js'; import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js'; import { resolveTag } from '../../../scripts/modules/utils.js';

View File

@@ -10,7 +10,7 @@ import {
handleApiResult, handleApiResult,
createErrorResponse, createErrorResponse,
withNormalizedProjectRoot withNormalizedProjectRoot
} from './utils.js'; } from '@tm/mcp';
import { analyzeTaskComplexityDirect } from '../core/task-master-core.js'; // Assuming core functions are exported via task-master-core.js import { analyzeTaskComplexityDirect } from '../core/task-master-core.js'; // Assuming core functions are exported via task-master-core.js
import { findTasksPath } from '../core/utils/path-utils.js'; import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js'; import { resolveTag } from '../../../scripts/modules/utils.js';

View File

@@ -4,11 +4,7 @@
*/ */
import { z } from 'zod'; import { z } from 'zod';
import { import { handleApiResult, createErrorResponse, withToolContext } from '@tm/mcp';
handleApiResult,
createErrorResponse,
withNormalizedProjectRoot
} from './utils.js';
import { clearSubtasksDirect } from '../core/task-master-core.js'; import { clearSubtasksDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js'; import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js'; import { resolveTag } from '../../../scripts/modules/utils.js';
@@ -43,9 +39,11 @@ export function registerClearSubtasksTool(server) {
message: "Either 'id' or 'all' parameter must be provided", message: "Either 'id' or 'all' parameter must be provided",
path: ['id', 'all'] path: ['id', 'all']
}), }),
execute: withNormalizedProjectRoot(async (args, { log, session }) => { execute: withToolContext('clear-subtasks', async (args, context) => {
try { try {
log.info(`Clearing subtasks with args: ${JSON.stringify(args)}`); context.log.info(
`Clearing subtasks with args: ${JSON.stringify(args)}`
);
const resolvedTag = resolveTag({ const resolvedTag = resolveTag({
projectRoot: args.projectRoot, projectRoot: args.projectRoot,
@@ -57,10 +55,10 @@ export function registerClearSubtasksTool(server) {
try { try {
tasksJsonPath = findTasksPath( tasksJsonPath = findTasksPath(
{ projectRoot: args.projectRoot, file: args.file }, { projectRoot: args.projectRoot, file: args.file },
log context.log
); );
} catch (error) { } catch (error) {
log.error(`Error finding tasks.json: ${error.message}`); context.log.error(`Error finding tasks.json: ${error.message}`);
return createErrorResponse( return createErrorResponse(
`Failed to find tasks.json: ${error.message}` `Failed to find tasks.json: ${error.message}`
); );
@@ -75,25 +73,29 @@ export function registerClearSubtasksTool(server) {
projectRoot: args.projectRoot, projectRoot: args.projectRoot,
tag: resolvedTag tag: resolvedTag
}, },
log, context.log,
{ session } { session: context.session }
); );
if (result.success) { if (result.success) {
log.info(`Subtasks cleared successfully: ${result.data.message}`); context.log.info(
`Subtasks cleared successfully: ${result.data.message}`
);
} else { } else {
log.error(`Failed to clear subtasks: ${result.error.message}`); context.log.error(
`Failed to clear subtasks: ${result.error.message}`
);
} }
return handleApiResult( return handleApiResult(
result, result,
log, context.log,
'Error clearing subtasks', 'Error clearing subtasks',
undefined, undefined,
args.projectRoot args.projectRoot
); );
} catch (error) { } catch (error) {
log.error(`Error in clearSubtasks tool: ${error.message}`); context.log.error(`Error in clearSubtasks tool: ${error.message}`);
return createErrorResponse(error.message); return createErrorResponse(error.message);
} }
}) })

View File

@@ -8,7 +8,7 @@ import {
handleApiResult, handleApiResult,
createErrorResponse, createErrorResponse,
withNormalizedProjectRoot withNormalizedProjectRoot
} from './utils.js'; } from '@tm/mcp';
import { complexityReportDirect } from '../core/task-master-core.js'; import { complexityReportDirect } from '../core/task-master-core.js';
import { COMPLEXITY_REPORT_FILE } from '../../../src/constants/paths.js'; import { COMPLEXITY_REPORT_FILE } from '../../../src/constants/paths.js';
import { findComplexityReportPath } from '../core/utils/path-utils.js'; import { findComplexityReportPath } from '../core/utils/path-utils.js';

View File

@@ -8,7 +8,7 @@ import {
createErrorResponse, createErrorResponse,
handleApiResult, handleApiResult,
withNormalizedProjectRoot withNormalizedProjectRoot
} from './utils.js'; } from '@tm/mcp';
import { copyTagDirect } from '../core/task-master-core.js'; import { copyTagDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js'; import { findTasksPath } from '../core/utils/path-utils.js';

View File

@@ -8,7 +8,7 @@ import {
createErrorResponse, createErrorResponse,
handleApiResult, handleApiResult,
withNormalizedProjectRoot withNormalizedProjectRoot
} from './utils.js'; } from '@tm/mcp';
import { deleteTagDirect } from '../core/task-master-core.js'; import { deleteTagDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js'; import { findTasksPath } from '../core/utils/path-utils.js';

View File

@@ -8,7 +8,7 @@ import {
handleApiResult, handleApiResult,
createErrorResponse, createErrorResponse,
withNormalizedProjectRoot withNormalizedProjectRoot
} from './utils.js'; } from '@tm/mcp';
import { expandAllTasksDirect } from '../core/task-master-core.js'; import { expandAllTasksDirect } from '../core/task-master-core.js';
import { import {
findTasksPath, findTasksPath,

View File

@@ -8,7 +8,7 @@ import {
handleApiResult, handleApiResult,
createErrorResponse, createErrorResponse,
withNormalizedProjectRoot withNormalizedProjectRoot
} from './utils.js'; } from '@tm/mcp';
import { expandTaskDirect } from '../core/task-master-core.js'; import { expandTaskDirect } from '../core/task-master-core.js';
import { import {
findTasksPath, findTasksPath,

View File

@@ -4,14 +4,11 @@
*/ */
import { z } from 'zod'; import { z } from 'zod';
import { import { handleApiResult, createErrorResponse, withToolContext } from '@tm/mcp';
handleApiResult,
createErrorResponse,
withNormalizedProjectRoot
} from './utils.js';
import { fixDependenciesDirect } from '../core/task-master-core.js'; import { fixDependenciesDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js'; import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js'; import { resolveTag } from '../../../scripts/modules/utils.js';
/** /**
* Register the fixDependencies tool with the MCP server * Register the fixDependencies tool with the MCP server
* @param {Object} server - FastMCP server instance * @param {Object} server - FastMCP server instance
@@ -27,9 +24,11 @@ export function registerFixDependenciesTool(server) {
.describe('The directory of the project. Must be an absolute path.'), .describe('The directory of the project. Must be an absolute path.'),
tag: z.string().optional().describe('Tag context to operate on') tag: z.string().optional().describe('Tag context to operate on')
}), }),
execute: withNormalizedProjectRoot(async (args, { log, session }) => { execute: withToolContext('fix-dependencies', async (args, context) => {
try { try {
log.info(`Fixing dependencies with args: ${JSON.stringify(args)}`); context.log.info(
`Fixing dependencies with args: ${JSON.stringify(args)}`
);
const resolvedTag = resolveTag({ const resolvedTag = resolveTag({
projectRoot: args.projectRoot, projectRoot: args.projectRoot,
@@ -41,10 +40,10 @@ export function registerFixDependenciesTool(server) {
try { try {
tasksJsonPath = findTasksPath( tasksJsonPath = findTasksPath(
{ projectRoot: args.projectRoot, file: args.file }, { projectRoot: args.projectRoot, file: args.file },
log context.log
); );
} catch (error) { } catch (error) {
log.error(`Error finding tasks.json: ${error.message}`); context.log.error(`Error finding tasks.json: ${error.message}`);
return createErrorResponse( return createErrorResponse(
`Failed to find tasks.json: ${error.message}` `Failed to find tasks.json: ${error.message}`
); );
@@ -56,24 +55,28 @@ export function registerFixDependenciesTool(server) {
projectRoot: args.projectRoot, projectRoot: args.projectRoot,
tag: resolvedTag tag: resolvedTag
}, },
log context.log
); );
if (result.success) { if (result.success) {
log.info(`Successfully fixed dependencies: ${result.data.message}`); context.log.info(
`Successfully fixed dependencies: ${result.data.message}`
);
} else { } else {
log.error(`Failed to fix dependencies: ${result.error.message}`); context.log.error(
`Failed to fix dependencies: ${result.error.message}`
);
} }
return handleApiResult( return handleApiResult(
result, result,
log, context.log,
'Error fixing dependencies', 'Error fixing dependencies',
undefined, undefined,
args.projectRoot args.projectRoot
); );
} catch (error) { } catch (error) {
log.error(`Error in fixDependencies tool: ${error.message}`); context.log.error(`Error in fixDependencies tool: ${error.message}`);
return createErrorResponse(error.message); return createErrorResponse(error.message);
} }
}) })

View File

@@ -1,6 +1,6 @@
// mcp-server/src/tools/get-operation-status.js // mcp-server/src/tools/get-operation-status.js
import { z } from 'zod'; import { z } from 'zod';
import { createErrorResponse, createContentResponse } from './utils.js'; // Assuming these utils exist import { createErrorResponse, createContentResponse } from '@tm/mcp'; // Assuming these utils exist
/** /**
* Register the get_operation_status tool. * Register the get_operation_status tool.

View File

@@ -3,7 +3,7 @@ import {
createErrorResponse, createErrorResponse,
handleApiResult, handleApiResult,
withNormalizedProjectRoot withNormalizedProjectRoot
} from './utils.js'; } from '@tm/mcp';
import { initializeProjectDirect } from '../core/task-master-core.js'; import { initializeProjectDirect } from '../core/task-master-core.js';
import { RULE_PROFILES } from '../../../src/constants/profiles.js'; import { RULE_PROFILES } from '../../../src/constants/profiles.js';

View File

@@ -8,7 +8,7 @@ import {
createErrorResponse, createErrorResponse,
handleApiResult, handleApiResult,
withNormalizedProjectRoot withNormalizedProjectRoot
} from './utils.js'; } from '@tm/mcp';
import { listTagsDirect } from '../core/task-master-core.js'; import { listTagsDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js'; import { findTasksPath } from '../core/utils/path-utils.js';

View File

@@ -4,11 +4,7 @@
*/ */
import { z } from 'zod'; import { z } from 'zod';
import { import { handleApiResult, createErrorResponse, withToolContext } from '@tm/mcp';
handleApiResult,
createErrorResponse,
withNormalizedProjectRoot
} from './utils.js';
import { modelsDirect } from '../core/task-master-core.js'; import { modelsDirect } from '../core/task-master-core.js';
/** /**
@@ -83,26 +79,28 @@ export function registerModelsTool(server) {
'Custom base URL for providers that support it (e.g., https://api.example.com/v1).' 'Custom base URL for providers that support it (e.g., https://api.example.com/v1).'
) )
}), }),
execute: withNormalizedProjectRoot(async (args, { log, session }) => { execute: withToolContext('models', async (args, context) => {
try { try {
log.info(`Starting models tool with args: ${JSON.stringify(args)}`); context.log.info(
`Starting models tool with args: ${JSON.stringify(args)}`
);
// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot) // Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
const result = await modelsDirect( const result = await modelsDirect(
{ ...args, projectRoot: args.projectRoot }, { ...args, projectRoot: args.projectRoot },
log, context.log,
{ session } { session: context.session }
); );
return handleApiResult( return handleApiResult(
result, result,
log, context.log,
'Error managing models', 'Error managing models',
undefined, undefined,
args.projectRoot args.projectRoot
); );
} catch (error) { } catch (error) {
log.error(`Error in models tool: ${error.message}`); context.log.error(`Error in models tool: ${error.message}`);
return createErrorResponse(error.message); return createErrorResponse(error.message);
} }
}) })

View File

@@ -8,7 +8,7 @@ import {
handleApiResult, handleApiResult,
createErrorResponse, createErrorResponse,
withNormalizedProjectRoot withNormalizedProjectRoot
} from './utils.js'; } from '@tm/mcp';
import { import {
moveTaskDirect, moveTaskDirect,
moveTaskCrossTagDirect moveTaskCrossTagDirect

View File

@@ -8,7 +8,7 @@ import {
createErrorResponse, createErrorResponse,
handleApiResult, handleApiResult,
withNormalizedProjectRoot withNormalizedProjectRoot
} from './utils.js'; } from '@tm/mcp';
import { nextTaskDirect } from '../core/task-master-core.js'; import { nextTaskDirect } from '../core/task-master-core.js';
import { import {
resolveTasksPath, resolveTasksPath,

View File

@@ -9,7 +9,7 @@ import {
withNormalizedProjectRoot, withNormalizedProjectRoot,
createErrorResponse, createErrorResponse,
checkProgressCapability checkProgressCapability
} from './utils.js'; } from '@tm/mcp';
import { parsePRDDirect } from '../core/task-master-core.js'; import { parsePRDDirect } from '../core/task-master-core.js';
import { import {
PRD_FILE, PRD_FILE,

View File

@@ -3,15 +3,11 @@
* Tool for removing a dependency from a task * Tool for removing a dependency from a task
*/ */
import { createErrorResponse, handleApiResult, withToolContext } from '@tm/mcp';
import { z } from 'zod'; import { z } from 'zod';
import { import { resolveTag } from '../../../scripts/modules/utils.js';
handleApiResult,
createErrorResponse,
withNormalizedProjectRoot
} from './utils.js';
import { removeDependencyDirect } from '../core/task-master-core.js'; import { removeDependencyDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js'; import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';
/** /**
* Register the removeDependency tool with the MCP server * Register the removeDependency tool with the MCP server
@@ -35,25 +31,25 @@ export function registerRemoveDependencyTool(server) {
.describe('The directory of the project. Must be an absolute path.'), .describe('The directory of the project. Must be an absolute path.'),
tag: z.string().optional().describe('Tag context to operate on') tag: z.string().optional().describe('Tag context to operate on')
}), }),
execute: withNormalizedProjectRoot(async (args, { log, session }) => { execute: withToolContext('remove-dependency', async (args, context) => {
try { try {
const resolvedTag = resolveTag({ const resolvedTag = resolveTag({
projectRoot: args.projectRoot, projectRoot: args.projectRoot,
tag: args.tag tag: args.tag
}); });
log.info( context.log.info(
`Removing dependency for task ${args.id} from ${args.dependsOn} with args: ${JSON.stringify(args)}` `Removing dependency for task ${args.id} from ${args.dependsOn} with args: ${JSON.stringify(args)}`
); );
// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot) // Use args.projectRoot directly (guaranteed by withToolContext)
let tasksJsonPath; let tasksJsonPath;
try { try {
tasksJsonPath = findTasksPath( tasksJsonPath = findTasksPath(
{ projectRoot: args.projectRoot, file: args.file }, { projectRoot: args.projectRoot, file: args.file },
log context.log
); );
} catch (error) { } catch (error) {
log.error(`Error finding tasks.json: ${error.message}`); context.log.error(`Error finding tasks.json: ${error.message}`);
return createErrorResponse( return createErrorResponse(
`Failed to find tasks.json: ${error.message}` `Failed to find tasks.json: ${error.message}`
); );
@@ -67,24 +63,28 @@ export function registerRemoveDependencyTool(server) {
projectRoot: args.projectRoot, projectRoot: args.projectRoot,
tag: resolvedTag tag: resolvedTag
}, },
log context.log
); );
if (result.success) { if (result.success) {
log.info(`Successfully removed dependency: ${result.data.message}`); context.log.info(
`Successfully removed dependency: ${result.data.message}`
);
} else { } else {
log.error(`Failed to remove dependency: ${result.error.message}`); context.log.error(
`Failed to remove dependency: ${result.error.message}`
);
} }
return handleApiResult( return handleApiResult(
result, result,
log, context.log,
'Error removing dependency', 'Error removing dependency',
undefined, undefined,
args.projectRoot args.projectRoot
); );
} catch (error) { } catch (error) {
log.error(`Error in removeDependency tool: ${error.message}`); context.log.error(`Error in removeDependency tool: ${error.message}`);
return createErrorResponse(error.message); return createErrorResponse(error.message);
} }
}) })

View File

@@ -8,7 +8,7 @@ import {
handleApiResult, handleApiResult,
createErrorResponse, createErrorResponse,
withNormalizedProjectRoot withNormalizedProjectRoot
} from './utils.js'; } from '@tm/mcp';
import { removeSubtaskDirect } from '../core/task-master-core.js'; import { removeSubtaskDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js'; import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js'; import { resolveTag } from '../../../scripts/modules/utils.js';

View File

@@ -8,7 +8,7 @@ import {
handleApiResult, handleApiResult,
createErrorResponse, createErrorResponse,
withNormalizedProjectRoot withNormalizedProjectRoot
} from './utils.js'; } from '@tm/mcp';
import { removeTaskDirect } from '../core/task-master-core.js'; import { removeTaskDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js'; import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js'; import { resolveTag } from '../../../scripts/modules/utils.js';

View File

@@ -8,7 +8,7 @@ import {
createErrorResponse, createErrorResponse,
handleApiResult, handleApiResult,
withNormalizedProjectRoot withNormalizedProjectRoot
} from './utils.js'; } from '@tm/mcp';
import { renameTagDirect } from '../core/task-master-core.js'; import { renameTagDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js'; import { findTasksPath } from '../core/utils/path-utils.js';

View File

@@ -8,7 +8,7 @@ import {
createErrorResponse, createErrorResponse,
handleApiResult, handleApiResult,
withNormalizedProjectRoot withNormalizedProjectRoot
} from './utils.js'; } from '@tm/mcp';
import { researchDirect } from '../core/task-master-core.js'; import { researchDirect } from '../core/task-master-core.js';
import { resolveTag } from '../../../scripts/modules/utils.js'; import { resolveTag } from '../../../scripts/modules/utils.js';

View File

@@ -3,7 +3,7 @@ import {
createErrorResponse, createErrorResponse,
handleApiResult, handleApiResult,
withNormalizedProjectRoot withNormalizedProjectRoot
} from './utils.js'; } from '@tm/mcp';
import { responseLanguageDirect } from '../core/direct-functions/response-language.js'; import { responseLanguageDirect } from '../core/direct-functions/response-language.js';
export function registerResponseLanguageTool(server) { export function registerResponseLanguageTool(server) {
@@ -36,7 +36,12 @@ export function registerResponseLanguageTool(server) {
log, log,
{ session } { session }
); );
return handleApiResult(result, log, 'Error setting response language'); return handleApiResult({
result,
log,
errorPrefix: 'Error setting response language',
projectRoot: args.projectRoot
});
} catch (error) { } catch (error) {
log.error(`Error in response-language tool: ${error.message}`); log.error(`Error in response-language tool: ${error.message}`);
return createErrorResponse(error.message); return createErrorResponse(error.message);

View File

@@ -8,7 +8,7 @@ import {
createErrorResponse, createErrorResponse,
handleApiResult, handleApiResult,
withNormalizedProjectRoot withNormalizedProjectRoot
} from './utils.js'; } from '@tm/mcp';
import { rulesDirect } from '../core/direct-functions/rules.js'; import { rulesDirect } from '../core/direct-functions/rules.js';
import { RULE_PROFILES } from '../../../src/constants/profiles.js'; import { RULE_PROFILES } from '../../../src/constants/profiles.js';
@@ -49,7 +49,11 @@ export function registerRulesTool(server) {
`[rules tool] Executing action: ${args.action} for profiles: ${args.profiles.join(', ')} in ${args.projectRoot}` `[rules tool] Executing action: ${args.action} for profiles: ${args.profiles.join(', ')} in ${args.projectRoot}`
); );
const result = await rulesDirect(args, log, { session }); const result = await rulesDirect(args, log, { session });
return handleApiResult(result, log); return handleApiResult({
result,
log,
projectRoot: args.projectRoot
});
} catch (error) { } catch (error) {
log.error(`[rules tool] Error: ${error.message}`); log.error(`[rules tool] Error: ${error.message}`);
return createErrorResponse(error.message, { details: error.stack }); return createErrorResponse(error.message, { details: error.stack });

View File

@@ -8,7 +8,7 @@ import {
createErrorResponse, createErrorResponse,
handleApiResult, handleApiResult,
withNormalizedProjectRoot withNormalizedProjectRoot
} from './utils.js'; } from '@tm/mcp';
import { scopeDownDirect } from '../core/task-master-core.js'; import { scopeDownDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js'; import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js'; import { resolveTag } from '../../../scripts/modules/utils.js';

View File

@@ -8,7 +8,7 @@ import {
createErrorResponse, createErrorResponse,
handleApiResult, handleApiResult,
withNormalizedProjectRoot withNormalizedProjectRoot
} from './utils.js'; } from '@tm/mcp';
import { scopeUpDirect } from '../core/task-master-core.js'; import { scopeUpDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js'; import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js'; import { resolveTag } from '../../../scripts/modules/utils.js';

View File

@@ -8,7 +8,7 @@ import {
handleApiResult, handleApiResult,
createErrorResponse, createErrorResponse,
withNormalizedProjectRoot withNormalizedProjectRoot
} from './utils.js'; } from '@tm/mcp';
import { import {
setTaskStatusDirect, setTaskStatusDirect,
nextTaskDirect nextTaskDirect

View File

@@ -8,7 +8,7 @@ import {
handleApiResult, handleApiResult,
createErrorResponse, createErrorResponse,
withNormalizedProjectRoot withNormalizedProjectRoot
} from './utils.js'; } from '@tm/mcp';
import { updateSubtaskByIdDirect } from '../core/task-master-core.js'; import { updateSubtaskByIdDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js'; import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js'; import { resolveTag } from '../../../scripts/modules/utils.js';

View File

@@ -8,7 +8,7 @@ import {
handleApiResult, handleApiResult,
createErrorResponse, createErrorResponse,
withNormalizedProjectRoot withNormalizedProjectRoot
} from './utils.js'; } from '@tm/mcp';
import { updateTaskByIdDirect } from '../core/task-master-core.js'; import { updateTaskByIdDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js'; import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js'; import { resolveTag } from '../../../scripts/modules/utils.js';

View File

@@ -8,7 +8,7 @@ import {
handleApiResult, handleApiResult,
createErrorResponse, createErrorResponse,
withNormalizedProjectRoot withNormalizedProjectRoot
} from './utils.js'; } from '@tm/mcp';
import { updateTasksDirect } from '../core/task-master-core.js'; import { updateTasksDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js'; import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js'; import { resolveTag } from '../../../scripts/modules/utils.js';

View File

@@ -8,7 +8,7 @@ import {
createErrorResponse, createErrorResponse,
handleApiResult, handleApiResult,
withNormalizedProjectRoot withNormalizedProjectRoot
} from './utils.js'; } from '@tm/mcp';
import { useTagDirect } from '../core/task-master-core.js'; import { useTagDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js'; import { findTasksPath } from '../core/utils/path-utils.js';

View File

@@ -3,15 +3,11 @@
* Tool for validating task dependencies * Tool for validating task dependencies
*/ */
import { createErrorResponse, handleApiResult, withToolContext } from '@tm/mcp';
import { z } from 'zod'; import { z } from 'zod';
import { import { resolveTag } from '../../../scripts/modules/utils.js';
handleApiResult,
createErrorResponse,
withNormalizedProjectRoot
} from './utils.js';
import { validateDependenciesDirect } from '../core/task-master-core.js'; import { validateDependenciesDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js'; import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';
/** /**
* Register the validateDependencies tool with the MCP server * Register the validateDependencies tool with the MCP server
@@ -29,56 +25,63 @@ export function registerValidateDependenciesTool(server) {
.describe('The directory of the project. Must be an absolute path.'), .describe('The directory of the project. Must be an absolute path.'),
tag: z.string().optional().describe('Tag context to operate on') tag: z.string().optional().describe('Tag context to operate on')
}), }),
execute: withNormalizedProjectRoot(async (args, { log, session }) => { execute: withToolContext(
try { 'validate-dependencies',
const resolvedTag = resolveTag({ async (args, { log, session }) => {
projectRoot: args.projectRoot,
tag: args.tag
});
log.info(`Validating dependencies with args: ${JSON.stringify(args)}`);
// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
let tasksJsonPath;
try { try {
tasksJsonPath = findTasksPath( const resolvedTag = resolveTag({
{ projectRoot: args.projectRoot, file: args.file }, projectRoot: args.projectRoot,
tag: args.tag
});
log.info(
`Validating dependencies with args: ${JSON.stringify(args)}`
);
// Use args.projectRoot directly (guaranteed by withToolContext)
let tasksJsonPath;
try {
tasksJsonPath = findTasksPath(
{ projectRoot: args.projectRoot, file: args.file },
log
);
} catch (error) {
log.error(`Error finding tasks.json: ${error.message}`);
return createErrorResponse(
`Failed to find tasks.json: ${error.message}`
);
}
const result = await validateDependenciesDirect(
{
tasksJsonPath: tasksJsonPath,
projectRoot: args.projectRoot,
tag: resolvedTag
},
log log
); );
} catch (error) {
log.error(`Error finding tasks.json: ${error.message}`);
return createErrorResponse(
`Failed to find tasks.json: ${error.message}`
);
}
const result = await validateDependenciesDirect( if (result.success) {
{ log.info(
tasksJsonPath: tasksJsonPath, `Successfully validated dependencies: ${result.data.message}`
);
} else {
log.error(
`Failed to validate dependencies: ${result.error.message}`
);
}
return handleApiResult({
result,
log,
errorPrefix: 'Error validating dependencies',
projectRoot: args.projectRoot, projectRoot: args.projectRoot,
tag: resolvedTag tag: resolvedTag
}, });
log } catch (error) {
); log.error(`Error in validateDependencies tool: ${error.message}`);
return createErrorResponse(error.message);
if (result.success) {
log.info(
`Successfully validated dependencies: ${result.data.message}`
);
} else {
log.error(`Failed to validate dependencies: ${result.error.message}`);
} }
return handleApiResult(
result,
log,
'Error validating dependencies',
undefined,
args.projectRoot
);
} catch (error) {
log.error(`Error in validateDependencies tool: ${error.message}`);
return createErrorResponse(error.message);
} }
}) )
}); });
} }

1707
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -82,6 +82,12 @@ export type {
} from './modules/auth/types.js'; } from './modules/auth/types.js';
export { AuthenticationError } from './modules/auth/types.js'; export { AuthenticationError } from './modules/auth/types.js';
// Auth constants
export {
LOCAL_ONLY_COMMANDS,
type LocalOnlyCommand
} from './modules/auth/index.js';
// Brief types // Brief types
export type { Brief } from './modules/briefs/types.js'; export type { Brief } from './modules/briefs/types.js';
export type { TagWithStats } from './modules/briefs/services/brief-service.js'; export type { TagWithStats } from './modules/briefs/services/brief-service.js';

View File

@@ -16,6 +16,7 @@ import type {
OAuthFlowOptions, OAuthFlowOptions,
UserContext UserContext
} from './types.js'; } from './types.js';
import { checkAuthBlock, type AuthBlockResult } from './command.guard.js';
/** /**
* Display information for storage context * Display information for storage context
@@ -225,6 +226,41 @@ export class AuthDomain {
return `${baseUrl}/home/${context.orgSlug}/briefs/create`; return `${baseUrl}/home/${context.orgSlug}/briefs/create`;
} }
// ========== Command Guards ==========
/**
* Check if a local-only command should be blocked when using API storage
*
* Local-only commands (like dependency management) are blocked when authenticated
* with Hamster and using API storage, since Hamster manages these features remotely.
*
* @param commandName - Name of the command to check
* @param storageType - Current storage type being used
* @returns Guard result with blocking decision and context
*
* @example
* ```ts
* const result = await tmCore.auth.guardCommand('add-dependency', tmCore.tasks.getStorageType());
* if (result.isBlocked) {
* console.log(`Command blocked: ${result.briefName}`);
* }
* ```
*/
async guardCommand(
commandName: string,
storageType: StorageType
): Promise<AuthBlockResult> {
const hasValidSession = await this.hasValidSession();
const context = this.getContext();
return checkAuthBlock({
hasValidSession,
briefName: context?.briefName,
storageType,
commandName
});
}
/** /**
* Get web app base URL from environment configuration * Get web app base URL from environment configuration
* @private * @private

View File

@@ -0,0 +1,77 @@
/**
* @fileoverview Command guard - Core logic for blocking local-only commands
* Pure business logic - no presentation layer concerns
*/
import type { StorageType } from '../../common/types/index.js';
import { LOCAL_ONLY_COMMANDS, type LocalOnlyCommand } from './constants.js';
/**
* Result from checking if a command should be blocked
*/
export interface AuthBlockResult {
/** Whether the command should be blocked */
isBlocked: boolean;
/** Brief name if authenticated with Hamster */
briefName?: string;
/** Command name that was checked */
commandName: string;
}
/**
* Check if a command is local-only
*/
export function isLocalOnlyCommand(
commandName: string
): commandName is LocalOnlyCommand {
return LOCAL_ONLY_COMMANDS.includes(commandName as LocalOnlyCommand);
}
/**
* Parameters for auth block check
*/
export interface AuthBlockParams {
/** Whether user has a valid auth session */
hasValidSession: boolean;
/** Brief name from auth context */
briefName?: string;
/** Current storage type being used */
storageType: StorageType;
/** Command name to check */
commandName: string;
}
/**
* Check if a command should be blocked because user is authenticated with Hamster
*
* This is pure business logic with dependency injection - returns data only, no display/formatting
* Presentation layers (CLI, MCP) should format the response appropriately
*
* @param params - Auth block parameters
* @returns AuthBlockResult with blocking decision and context
*/
export function checkAuthBlock(params: AuthBlockParams): AuthBlockResult {
const { hasValidSession, briefName, storageType, commandName } = params;
// Only check auth for local-only commands
if (!isLocalOnlyCommand(commandName)) {
return { isBlocked: false, commandName };
}
// Not authenticated - command is allowed
if (!hasValidSession) {
return { isBlocked: false, commandName };
}
// Authenticated but using file storage - command is allowed
if (storageType !== 'api') {
return { isBlocked: false, commandName };
}
// User is authenticated AND using API storage - block the command
return {
isBlocked: true,
briefName: briefName || 'remote brief',
commandName
};
}

View File

@@ -0,0 +1,18 @@
/**
* @fileoverview Auth module constants
*/
/**
* Commands that are only available for local file storage
* These commands are blocked when using Hamster (API storage)
*/
export const LOCAL_ONLY_COMMANDS = [
'add-dependency',
'remove-dependency',
'validate-dependencies',
'fix-dependencies',
'clear-subtasks',
'models'
] as const;
export type LocalOnlyCommand = (typeof LOCAL_ONLY_COMMANDS)[number];

View File

@@ -26,3 +26,9 @@ export {
DEFAULT_AUTH_CONFIG, DEFAULT_AUTH_CONFIG,
getAuthConfig getAuthConfig
} from './config.js'; } from './config.js';
// Command guard types and utilities
export { isLocalOnlyCommand, type AuthBlockResult } from './command.guard.js';
// Auth constants
export { LOCAL_ONLY_COMMANDS, type LocalOnlyCommand } from './constants.js';

View File

@@ -3,139 +3,142 @@
* Command-line interface for the Task Master CLI * Command-line interface for the Task Master CLI
*/ */
import { Command } from 'commander';
import path from 'path';
import chalk from 'chalk';
import boxen from 'boxen';
import fs from 'fs'; import fs from 'fs';
import path from 'path';
import boxen from 'boxen';
import chalk from 'chalk';
import { Command } from 'commander';
import inquirer from 'inquirer'; import inquirer from 'inquirer';
import { log, readJSON } from './utils.js';
// Import command registry and utilities from @tm/cli // Import command registry and utilities from @tm/cli
import { import {
registerAllCommands,
checkForUpdate, checkForUpdate,
performAutoUpdate,
displayUpgradeNotification,
restartWithNewVersion,
displayError, displayError,
displayUpgradeNotification,
performAutoUpdate,
registerAllCommands,
restartWithNewVersion,
runInteractiveSetup runInteractiveSetup
} from '@tm/cli'; } from '@tm/cli';
import { log, readJSON } from './utils.js';
import { import {
parsePRD,
updateTasks,
expandTask,
expandAllTasks,
clearSubtasks,
addTask,
addSubtask, addSubtask,
removeSubtask, addTask,
analyzeTaskComplexity, analyzeTaskComplexity,
updateTaskById, clearSubtasks,
updateSubtaskById, expandAllTasks,
removeTask, expandTask,
findTaskById, findTaskById,
taskExists,
moveTask,
migrateProject, migrateProject,
setResponseLanguage, moveTask,
scopeUpTask, parsePRD,
removeSubtask,
removeTask,
scopeDownTask, scopeDownTask,
scopeUpTask,
setResponseLanguage,
taskExists,
updateSubtaskById,
updateTaskById,
updateTasks,
validateStrength validateStrength
} from './task-manager.js'; } from './task-manager.js';
import { moveTasksBetweenTags } from './task-manager/move-task.js'; import { moveTasksBetweenTags } from './task-manager/move-task.js';
import { import {
copyTag,
createTag, createTag,
deleteTag, deleteTag,
tags,
useTag,
renameTag, renameTag,
copyTag tags,
useTag
} from './task-manager/tag-management.js'; } from './task-manager/tag-management.js';
import { import {
addDependency, addDependency,
fixDependenciesCommand,
removeDependency, removeDependency,
validateDependenciesCommand, validateDependenciesCommand
fixDependenciesCommand
} from './dependency-manager.js'; } from './dependency-manager.js';
import { checkAndBlockIfAuthenticated } from '@tm/cli';
import { LOCAL_ONLY_COMMANDS } from '@tm/core';
import { import {
isApiKeySet,
getDebugFlag,
ConfigurationError, ConfigurationError,
isConfigFilePresent, getDebugFlag,
getDefaultNumTasks getDefaultNumTasks,
isApiKeySet,
isConfigFilePresent
} from './config-manager.js'; } from './config-manager.js';
import { CUSTOM_PROVIDERS } from '@tm/core'; import { CUSTOM_PROVIDERS } from '@tm/core';
import { import {
COMPLEXITY_REPORT_FILE, COMPLEXITY_REPORT_FILE,
TASKMASTER_TASKS_FILE, TASKMASTER_DOCS_DIR,
TASKMASTER_DOCS_DIR TASKMASTER_TASKS_FILE
} from '../../src/constants/paths.js'; } from '../../src/constants/paths.js';
import { initTaskMaster } from '../../src/task-master.js'; import { initTaskMaster } from '../../src/task-master.js';
import {
displayBanner,
displayHelp,
displayComplexityReport,
getStatusWithColor,
confirmTaskOverwrite,
startLoadingIndicator,
stopLoadingIndicator,
displayModelConfiguration,
displayAvailableModels,
displayApiKeyStatus,
displayTaggedTasksFYI,
displayCurrentTagIndicator,
displayCrossTagDependencyError,
displaySubtaskMoveError,
displayInvalidTagCombinationError,
displayDependencyValidationHints
} from './ui.js';
import { import {
confirmProfilesRemove, confirmProfilesRemove,
confirmRemoveAllRemainingProfiles confirmRemoveAllRemainingProfiles
} from '../../src/ui/confirm.js'; } from '../../src/ui/confirm.js';
import { import {
wouldRemovalLeaveNoProfiles, getInstalledProfiles,
getInstalledProfiles wouldRemovalLeaveNoProfiles
} from '../../src/utils/profiles.js'; } from '../../src/utils/profiles.js';
import {
confirmTaskOverwrite,
displayApiKeyStatus,
displayAvailableModels,
displayBanner,
displayComplexityReport,
displayCrossTagDependencyError,
displayCurrentTagIndicator,
displayDependencyValidationHints,
displayHelp,
displayInvalidTagCombinationError,
displayModelConfiguration,
displaySubtaskMoveError,
displayTaggedTasksFYI,
getStatusWithColor,
startLoadingIndicator,
stopLoadingIndicator
} from './ui.js';
import { initializeProject } from '../init.js';
import {
getModelConfiguration,
getAvailableModelsList,
setModel,
getApiKeyStatusReport
} from './task-manager/models.js';
import {
isValidRulesAction,
RULES_ACTIONS,
RULES_SETUP_ACTION
} from '../../src/constants/rules-actions.js';
import { getTaskMasterVersion } from '../../src/utils/getVersion.js';
import { syncTasksToReadme } from './sync-readme.js';
import { RULE_PROFILES } from '../../src/constants/profiles.js'; import { RULE_PROFILES } from '../../src/constants/profiles.js';
import { import {
convertAllRulesToProfileRules, RULES_ACTIONS,
removeProfileRules, RULES_SETUP_ACTION,
isValidProfile, isValidRulesAction
getRulesProfile } from '../../src/constants/rules-actions.js';
} from '../../src/utils/rule-transformer.js'; import { getTaskMasterVersion } from '../../src/utils/getVersion.js';
import { import {
runInteractiveProfilesSetup,
generateProfileSummary,
categorizeProfileResults, categorizeProfileResults,
categorizeRemovalResults,
generateProfileRemovalSummary, generateProfileRemovalSummary,
categorizeRemovalResults generateProfileSummary,
runInteractiveProfilesSetup
} from '../../src/utils/profiles.js'; } from '../../src/utils/profiles.js';
import {
convertAllRulesToProfileRules,
getRulesProfile,
isValidProfile,
removeProfileRules
} from '../../src/utils/rule-transformer.js';
import { initializeProject } from '../init.js';
import { syncTasksToReadme } from './sync-readme.js';
import {
getApiKeyStatusReport,
getAvailableModelsList,
getModelConfiguration,
setModel
} from './task-manager/models.js';
/** /**
* Configure and register CLI commands * Configure and register CLI commands
@@ -154,6 +157,23 @@ function registerCommands(programInstance) {
process.exit(1); process.exit(1);
}); });
// Add global command guard for local-only commands
programInstance.hook('preAction', async (thisCommand, actionCommand) => {
const commandName = actionCommand.name();
// Only check if it's a local-only command
if (LOCAL_ONLY_COMMANDS.includes(commandName)) {
const taskMaster = initTaskMaster(actionCommand.opts());
const isBlocked = await checkAndBlockIfAuthenticated(
commandName,
taskMaster.getProjectRoot()
);
if (isBlocked) {
process.exit(1);
}
}
});
// parse-prd command // parse-prd command
programInstance programInstance
.command('parse-prd') .command('parse-prd')