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 chalk from 'chalk';
/**
* Level/variant for the card box styling
*/
export type CardBoxLevel = 'warn' | 'info';
/**
* Configuration for the card box component
*/
export interface CardBoxConfig {
/** Header text displayed in yellow bold */
/** Header text displayed in bold */
header: string;
/** Body paragraphs displayed in white */
body: string[];
/** Call to action section with label and URL */
callToAction: {
/** Call to action section with label and URL (optional) */
callToAction?: {
label: string;
action: string;
};
/** Footer text displayed in gray (usage instructions) */
footer?: string;
/** Level/variant for styling (default: 'warn' = yellow, 'info' = blue) */
level?: CardBoxLevel;
}
/**
@@ -26,21 +33,29 @@ export interface CardBoxConfig {
* @returns Formatted string ready for console.log
*/
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
const sections: string[] = [
// Header
chalk.yellow.bold(header),
headerColor(header),
// Body paragraphs
...body.map((paragraph) => chalk.white(paragraph)),
...body.map((paragraph) => chalk.white(paragraph))
];
// Call to 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
if (footer) {
@@ -53,7 +68,7 @@ export function displayCardBox(config: CardBoxConfig): string {
// Wrap in boxen
return boxen(content, {
padding: 1,
borderColor: 'yellow',
borderColor,
borderStyle: 'round',
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
} from './auth-helpers.js';
// Command guard for local-only commands
export {
checkAndBlockIfAuthenticated,
checkAndBlockDependencyCommand // Legacy export
} from './command-guard.js';
// Error handling utilities
export { displayError, isDebugMode } from './error-handler.js';

View File

@@ -5,5 +5,7 @@
export * from './tools/autopilot/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/types.js';

View File

@@ -2,6 +2,8 @@
* Shared types for MCP tools
*/
import type { TmCore } from '@tm/core';
export interface MCPResponse<T = any> {
success: boolean;
data?: T;
@@ -31,6 +33,29 @@ export interface MCPContext {
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 {
projectRoot: string;
}

View File

@@ -2,10 +2,16 @@
* Shared utilities for MCP tools
*/
import type { ContentResult } from 'fastmcp';
import path from 'node:path';
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 type { ToolContext } from './types.js';
/**
* 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
*/
@@ -183,7 +265,7 @@ function getProjectRootFromSession(session: any): string | null {
export function withNormalizedProjectRoot<T extends { projectRoot?: string }>(
fn: (
args: T & { projectRoot: string },
context: any
context: Context<undefined>
) => Promise<ContentResult>
): (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
*/
import { z } from 'zod';
import {
handleApiResult,
withNormalizedProjectRoot
} from '../../shared/utils.js';
import type { MCPContext } from '../../shared/types.js';
import { WorkflowService } from '@tm/core';
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({
projectRoot: z
@@ -29,12 +26,13 @@ export function registerAutopilotAbortTool(server: FastMCP) {
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.',
parameters: AbortSchema,
execute: withNormalizedProjectRoot(
async (args: AbortArgs, context: MCPContext) => {
execute: withToolContext(
'autopilot-abort',
async (args: AbortArgs, { log }: ToolContext) => {
const { projectRoot } = args;
try {
context.log.info(`Aborting autopilot workflow in ${projectRoot}`);
log.info(`Aborting autopilot workflow in ${projectRoot}`);
const workflowService = new WorkflowService(projectRoot);
@@ -42,7 +40,7 @@ export function registerAutopilotAbortTool(server: FastMCP) {
const hasWorkflow = await workflowService.hasWorkflow();
if (!hasWorkflow) {
context.log.warn('No active workflow to abort');
log.warn('No active workflow to abort');
return handleApiResult({
result: {
success: true,
@@ -51,7 +49,7 @@ export function registerAutopilotAbortTool(server: FastMCP) {
hadWorkflow: false
}
},
log: context.log,
log,
projectRoot
});
}
@@ -63,7 +61,7 @@ export function registerAutopilotAbortTool(server: FastMCP) {
// Abort workflow
await workflowService.abortWorkflow();
context.log.info('Workflow state deleted');
log.info('Workflow state deleted');
return handleApiResult({
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.'
}
},
log: context.log,
log,
projectRoot
});
} catch (error: any) {
context.log.error(`Error in autopilot-abort: ${error.message}`);
log.error(`Error in autopilot-abort: ${error.message}`);
if (error.stack) {
context.log.debug(error.stack);
log.debug(error.stack);
}
return handleApiResult({
result: {
success: false,
error: { message: `Failed to abort workflow: ${error.message}` }
},
log: context.log,
log,
projectRoot
});
}

View File

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

View File

@@ -3,14 +3,11 @@
* 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 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({
projectRoot: z
@@ -37,14 +34,13 @@ export function registerAutopilotCompleteTool(server: FastMCP) {
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.',
parameters: CompletePhaseSchema,
execute: withNormalizedProjectRoot(
async (args: CompletePhaseArgs, context: MCPContext) => {
execute: withToolContext(
'autopilot-complete-phase',
async (args: CompletePhaseArgs, { log }: ToolContext) => {
const { projectRoot, testResults } = args;
try {
context.log.info(
`Completing current phase in workflow for ${projectRoot}`
);
log.info(`Completing current phase in workflow for ${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'
}
},
log: context.log,
log,
projectRoot
});
}
@@ -76,7 +72,7 @@ export function registerAutopilotCompleteTool(server: FastMCP) {
message: `Cannot complete phase: not in a TDD phase (current phase: ${currentStatus.phase})`
}
},
log: context.log,
log,
projectRoot
});
}
@@ -91,7 +87,7 @@ export function registerAutopilotCompleteTool(server: FastMCP) {
'Cannot complete COMMIT phase with this tool. Use autopilot_commit instead'
}
},
log: context.log,
log,
projectRoot
});
}
@@ -112,7 +108,7 @@ export function registerAutopilotCompleteTool(server: FastMCP) {
const status = await workflowService.completePhase(fullTestResults);
const nextAction = workflowService.getNextAction();
context.log.info(
log.info(
`Phase completed. New phase: ${status.tddPhase || status.phase}`
);
@@ -127,13 +123,13 @@ export function registerAutopilotCompleteTool(server: FastMCP) {
nextSteps: nextAction.nextSteps
}
},
log: context.log,
log,
projectRoot
});
} catch (error: any) {
context.log.error(`Error in autopilot-complete: ${error.message}`);
log.error(`Error in autopilot-complete: ${error.message}`);
if (error.stack) {
context.log.debug(error.stack);
log.debug(error.stack);
}
return handleApiResult({
result: {
@@ -142,7 +138,7 @@ export function registerAutopilotCompleteTool(server: FastMCP) {
message: `Failed to complete phase: ${error.message}`
}
},
log: context.log,
log,
projectRoot
});
}

View File

@@ -3,14 +3,11 @@
* 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 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({
projectRoot: z
@@ -29,12 +26,13 @@ export function registerAutopilotFinalizeTool(server: FastMCP) {
description:
'Finalize and complete the workflow. Validates that all changes are committed and working tree is clean before marking workflow as complete.',
parameters: FinalizeSchema,
execute: withNormalizedProjectRoot(
async (args: FinalizeArgs, context: MCPContext) => {
execute: withToolContext(
'autopilot-finalize',
async (args: FinalizeArgs, { log }: ToolContext) => {
const { projectRoot } = args;
try {
context.log.info(`Finalizing workflow in ${projectRoot}`);
log.info(`Finalizing workflow in ${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'
}
},
log: context.log,
log,
projectRoot
});
}
@@ -66,7 +64,7 @@ export function registerAutopilotFinalizeTool(server: FastMCP) {
message: `Cannot finalize: workflow is in ${currentStatus.phase} phase. Complete all subtasks first.`
}
},
log: context.log,
log,
projectRoot
});
}
@@ -74,7 +72,7 @@ export function registerAutopilotFinalizeTool(server: FastMCP) {
// Finalize workflow (validates clean working tree)
const newStatus = await workflowService.finalizeWorkflow();
context.log.info('Workflow finalized successfully');
log.info('Workflow finalized successfully');
// Get next action
const nextAction = workflowService.getNextAction();
@@ -89,13 +87,13 @@ export function registerAutopilotFinalizeTool(server: FastMCP) {
nextSteps: nextAction.nextSteps
}
},
log: context.log,
log,
projectRoot
});
} catch (error: any) {
context.log.error(`Error in autopilot-finalize: ${error.message}`);
log.error(`Error in autopilot-finalize: ${error.message}`);
if (error.stack) {
context.log.debug(error.stack);
log.debug(error.stack);
}
return handleApiResult({
result: {
@@ -104,7 +102,7 @@ export function registerAutopilotFinalizeTool(server: FastMCP) {
message: `Failed to finalize workflow: ${error.message}`
}
},
log: context.log,
log,
projectRoot
});
}

View File

@@ -3,14 +3,11 @@
* 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 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({
projectRoot: z
@@ -29,14 +26,13 @@ export function registerAutopilotNextTool(server: FastMCP) {
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.',
parameters: NextActionSchema,
execute: withNormalizedProjectRoot(
async (args: NextActionArgs, context: MCPContext) => {
execute: withToolContext(
'autopilot-next',
async (args: NextActionArgs, { log }: ToolContext) => {
const { projectRoot } = args;
try {
context.log.info(
`Getting next action for workflow in ${projectRoot}`
);
log.info(`Getting next action for workflow in ${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'
}
},
log: context.log,
log,
projectRoot
});
}
@@ -62,7 +58,7 @@ export function registerAutopilotNextTool(server: FastMCP) {
const nextAction = workflowService.getNextAction();
const status = workflowService.getStatus();
context.log.info(`Next action determined: ${nextAction.action}`);
log.info(`Next action determined: ${nextAction.action}`);
return handleApiResult({
result: {
@@ -74,13 +70,13 @@ export function registerAutopilotNextTool(server: FastMCP) {
nextSteps: nextAction.nextSteps
}
},
log: context.log,
log,
projectRoot
});
} catch (error: any) {
context.log.error(`Error in autopilot-next: ${error.message}`);
log.error(`Error in autopilot-next: ${error.message}`);
if (error.stack) {
context.log.debug(error.stack);
log.debug(error.stack);
}
return handleApiResult({
result: {
@@ -89,7 +85,7 @@ export function registerAutopilotNextTool(server: FastMCP) {
message: `Failed to get next action: ${error.message}`
}
},
log: context.log,
log,
projectRoot
});
}

View File

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

View File

@@ -4,12 +4,8 @@
*/
import { z } from 'zod';
import {
handleApiResult,
withNormalizedProjectRoot
} from '../../shared/utils.js';
import type { MCPContext } from '../../shared/types.js';
import { createTmCore } from '@tm/core';
import { handleApiResult, withToolContext } from '../../shared/utils.js';
import type { ToolContext } from '../../shared/types.js';
import { WorkflowService } from '@tm/core';
import type { FastMCP } from 'fastmcp';
@@ -57,12 +53,13 @@ export function registerAutopilotStartTool(server: FastMCP) {
description:
'Initialize and start a new TDD workflow for a task. Creates a git branch and sets up the workflow state machine.',
parameters: StartWorkflowSchema,
execute: withNormalizedProjectRoot(
async (args: StartWorkflowArgs, context: MCPContext) => {
execute: withToolContext(
'autopilot-start',
async (args: StartWorkflowArgs, { log, tmCore }: ToolContext) => {
const { taskId, projectRoot, maxAttempts, force } = args;
try {
context.log.info(
log.info(
`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.`
}
},
log: context.log,
log,
projectRoot
});
}
// Load task data and get current tag
const core = await createTmCore({
projectPath: projectRoot
});
// 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) {
return handleApiResult({
@@ -96,7 +88,7 @@ export function registerAutopilotStartTool(server: FastMCP) {
success: false,
error: { message: `Task ${taskId} not found` }
},
log: context.log,
log,
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.`
}
},
log: context.log,
log,
projectRoot
});
}
@@ -123,7 +115,7 @@ export function registerAutopilotStartTool(server: FastMCP) {
// Check for existing workflow
const hasWorkflow = await workflowService.hasWorkflow();
if (hasWorkflow && !force) {
context.log.warn('Workflow state already exists');
log.warn('Workflow state already exists');
return handleApiResult({
result: {
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'
}
},
log: context.log,
log,
projectRoot
});
}
@@ -152,7 +144,7 @@ export function registerAutopilotStartTool(server: FastMCP) {
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
const nextAction = workflowService.getNextAction();
@@ -172,20 +164,20 @@ export function registerAutopilotStartTool(server: FastMCP) {
nextSteps: nextAction.nextSteps
}
},
log: context.log,
log,
projectRoot
});
} catch (error: any) {
context.log.error(`Error in autopilot-start: ${error.message}`);
log.error(`Error in autopilot-start: ${error.message}`);
if (error.stack) {
context.log.debug(error.stack);
log.debug(error.stack);
}
return handleApiResult({
result: {
success: false,
error: { message: `Failed to start workflow: ${error.message}` }
},
log: context.log,
log,
projectRoot
});
}

View File

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

View File

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

View File

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

View File

@@ -3,15 +3,11 @@
* Tool for adding a dependency to a task
*/
import { createErrorResponse, handleApiResult, withToolContext } from '@tm/mcp';
import { z } from 'zod';
import {
handleApiResult,
createErrorResponse,
withNormalizedProjectRoot
} from './utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';
import { addDependencyDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';
/**
* Register the addDependency tool with the MCP server
@@ -37,7 +33,9 @@ export function registerAddDependencyTool(server) {
.describe('The directory of the project. Must be an absolute path.'),
tag: z.string().optional().describe('Tag context to operate on')
}),
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
execute: withToolContext(
'add-dependency',
async (args, { log, session }) => {
try {
log.info(
`Adding dependency for task ${args.id} to depend on ${args.dependsOn}`
@@ -82,17 +80,18 @@ export function registerAddDependencyTool(server) {
}
// Use handleApiResult to format the response
return handleApiResult(
return handleApiResult({
result,
log,
'Error adding dependency',
undefined,
args.projectRoot
);
errorPrefix: 'Error adding dependency',
projectRoot: args.projectRoot,
tag: resolvedTag
});
} catch (error) {
log.error(`Error in addDependency tool: ${error.message}`);
return createErrorResponse(error.message);
}
})
}
)
});
}

View File

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

View File

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

View File

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

View File

@@ -10,7 +10,7 @@ import {
handleApiResult,
createErrorResponse,
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 { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
// mcp-server/src/tools/get-operation-status.js
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.

View File

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

View File

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

View File

@@ -4,11 +4,7 @@
*/
import { z } from 'zod';
import {
handleApiResult,
createErrorResponse,
withNormalizedProjectRoot
} from './utils.js';
import { handleApiResult, createErrorResponse, withToolContext } from '@tm/mcp';
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).'
)
}),
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
execute: withToolContext('models', async (args, context) => {
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)
const result = await modelsDirect(
{ ...args, projectRoot: args.projectRoot },
log,
{ session }
context.log,
{ session: context.session }
);
return handleApiResult(
result,
log,
context.log,
'Error managing models',
undefined,
args.projectRoot
);
} catch (error) {
log.error(`Error in models tool: ${error.message}`);
context.log.error(`Error in models tool: ${error.message}`);
return createErrorResponse(error.message);
}
})

View File

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

View File

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

View File

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

View File

@@ -3,15 +3,11 @@
* Tool for removing a dependency from a task
*/
import { createErrorResponse, handleApiResult, withToolContext } from '@tm/mcp';
import { z } from 'zod';
import {
handleApiResult,
createErrorResponse,
withNormalizedProjectRoot
} from './utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';
import { removeDependencyDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';
/**
* 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.'),
tag: z.string().optional().describe('Tag context to operate on')
}),
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
execute: withToolContext('remove-dependency', async (args, context) => {
try {
const resolvedTag = resolveTag({
projectRoot: args.projectRoot,
tag: args.tag
});
log.info(
context.log.info(
`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;
try {
tasksJsonPath = findTasksPath(
{ projectRoot: args.projectRoot, file: args.file },
log
context.log
);
} catch (error) {
log.error(`Error finding tasks.json: ${error.message}`);
context.log.error(`Error finding tasks.json: ${error.message}`);
return createErrorResponse(
`Failed to find tasks.json: ${error.message}`
);
@@ -67,24 +63,28 @@ export function registerRemoveDependencyTool(server) {
projectRoot: args.projectRoot,
tag: resolvedTag
},
log
context.log
);
if (result.success) {
log.info(`Successfully removed dependency: ${result.data.message}`);
context.log.info(
`Successfully removed dependency: ${result.data.message}`
);
} else {
log.error(`Failed to remove dependency: ${result.error.message}`);
context.log.error(
`Failed to remove dependency: ${result.error.message}`
);
}
return handleApiResult(
result,
log,
context.log,
'Error removing dependency',
undefined,
args.projectRoot
);
} catch (error) {
log.error(`Error in removeDependency tool: ${error.message}`);
context.log.error(`Error in removeDependency tool: ${error.message}`);
return createErrorResponse(error.message);
}
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,7 +8,7 @@ import {
createErrorResponse,
handleApiResult,
withNormalizedProjectRoot
} from './utils.js';
} from '@tm/mcp';
import { rulesDirect } from '../core/direct-functions/rules.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}`
);
const result = await rulesDirect(args, log, { session });
return handleApiResult(result, log);
return handleApiResult({
result,
log,
projectRoot: args.projectRoot
});
} catch (error) {
log.error(`[rules tool] Error: ${error.message}`);
return createErrorResponse(error.message, { details: error.stack });

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,15 +3,11 @@
* Tool for validating task dependencies
*/
import { createErrorResponse, handleApiResult, withToolContext } from '@tm/mcp';
import { z } from 'zod';
import {
handleApiResult,
createErrorResponse,
withNormalizedProjectRoot
} from './utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';
import { validateDependenciesDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';
/**
* Register the validateDependencies tool with the MCP server
@@ -29,15 +25,19 @@ export function registerValidateDependenciesTool(server) {
.describe('The directory of the project. Must be an absolute path.'),
tag: z.string().optional().describe('Tag context to operate on')
}),
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
execute: withToolContext(
'validate-dependencies',
async (args, { log, session }) => {
try {
const resolvedTag = resolveTag({
projectRoot: args.projectRoot,
tag: args.tag
});
log.info(`Validating dependencies with args: ${JSON.stringify(args)}`);
log.info(
`Validating dependencies with args: ${JSON.stringify(args)}`
);
// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
// Use args.projectRoot directly (guaranteed by withToolContext)
let tasksJsonPath;
try {
tasksJsonPath = findTasksPath(
@@ -65,20 +65,23 @@ export function registerValidateDependenciesTool(server) {
`Successfully validated dependencies: ${result.data.message}`
);
} else {
log.error(`Failed to validate dependencies: ${result.error.message}`);
log.error(
`Failed to validate dependencies: ${result.error.message}`
);
}
return handleApiResult(
return handleApiResult({
result,
log,
'Error validating dependencies',
undefined,
args.projectRoot
);
errorPrefix: 'Error validating dependencies',
projectRoot: args.projectRoot,
tag: resolvedTag
});
} 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';
export { AuthenticationError } from './modules/auth/types.js';
// Auth constants
export {
LOCAL_ONLY_COMMANDS,
type LocalOnlyCommand
} from './modules/auth/index.js';
// Brief types
export type { Brief } from './modules/briefs/types.js';
export type { TagWithStats } from './modules/briefs/services/brief-service.js';

View File

@@ -16,6 +16,7 @@ import type {
OAuthFlowOptions,
UserContext
} from './types.js';
import { checkAuthBlock, type AuthBlockResult } from './command.guard.js';
/**
* Display information for storage context
@@ -225,6 +226,41 @@ export class AuthDomain {
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
* @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,
getAuthConfig
} 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
*/
import { Command } from 'commander';
import path from 'path';
import chalk from 'chalk';
import boxen from 'boxen';
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 { log, readJSON } from './utils.js';
// Import command registry and utilities from @tm/cli
import {
registerAllCommands,
checkForUpdate,
performAutoUpdate,
displayUpgradeNotification,
restartWithNewVersion,
displayError,
displayUpgradeNotification,
performAutoUpdate,
registerAllCommands,
restartWithNewVersion,
runInteractiveSetup
} from '@tm/cli';
import { log, readJSON } from './utils.js';
import {
parsePRD,
updateTasks,
expandTask,
expandAllTasks,
clearSubtasks,
addTask,
addSubtask,
removeSubtask,
addTask,
analyzeTaskComplexity,
updateTaskById,
updateSubtaskById,
removeTask,
clearSubtasks,
expandAllTasks,
expandTask,
findTaskById,
taskExists,
moveTask,
migrateProject,
setResponseLanguage,
scopeUpTask,
moveTask,
parsePRD,
removeSubtask,
removeTask,
scopeDownTask,
scopeUpTask,
setResponseLanguage,
taskExists,
updateSubtaskById,
updateTaskById,
updateTasks,
validateStrength
} from './task-manager.js';
import { moveTasksBetweenTags } from './task-manager/move-task.js';
import {
copyTag,
createTag,
deleteTag,
tags,
useTag,
renameTag,
copyTag
tags,
useTag
} from './task-manager/tag-management.js';
import {
addDependency,
fixDependenciesCommand,
removeDependency,
validateDependenciesCommand,
fixDependenciesCommand
validateDependenciesCommand
} from './dependency-manager.js';
import { checkAndBlockIfAuthenticated } from '@tm/cli';
import { LOCAL_ONLY_COMMANDS } from '@tm/core';
import {
isApiKeySet,
getDebugFlag,
ConfigurationError,
isConfigFilePresent,
getDefaultNumTasks
getDebugFlag,
getDefaultNumTasks,
isApiKeySet,
isConfigFilePresent
} from './config-manager.js';
import { CUSTOM_PROVIDERS } from '@tm/core';
import {
COMPLEXITY_REPORT_FILE,
TASKMASTER_TASKS_FILE,
TASKMASTER_DOCS_DIR
TASKMASTER_DOCS_DIR,
TASKMASTER_TASKS_FILE
} from '../../src/constants/paths.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 {
confirmProfilesRemove,
confirmRemoveAllRemainingProfiles
} from '../../src/ui/confirm.js';
import {
wouldRemovalLeaveNoProfiles,
getInstalledProfiles
getInstalledProfiles,
wouldRemovalLeaveNoProfiles
} 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 {
convertAllRulesToProfileRules,
removeProfileRules,
isValidProfile,
getRulesProfile
} from '../../src/utils/rule-transformer.js';
RULES_ACTIONS,
RULES_SETUP_ACTION,
isValidRulesAction
} from '../../src/constants/rules-actions.js';
import { getTaskMasterVersion } from '../../src/utils/getVersion.js';
import {
runInteractiveProfilesSetup,
generateProfileSummary,
categorizeProfileResults,
categorizeRemovalResults,
generateProfileRemovalSummary,
categorizeRemovalResults
generateProfileSummary,
runInteractiveProfilesSetup
} 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
@@ -154,6 +157,23 @@ function registerCommands(programInstance) {
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
programInstance
.command('parse-prd')