mirror of
https://github.com/eyaltoledano/claude-task-master.git
synced 2026-01-30 06:12:05 +00:00
fix: get tasks (#1385)
* fix: get tasks by calling an endpoint * chore: apply requested changes
This commit is contained in:
@@ -7,7 +7,8 @@ import {
|
||||
OAuthFlowOptions,
|
||||
AuthenticationError,
|
||||
AuthConfig,
|
||||
UserContext
|
||||
UserContext,
|
||||
UserContextWithBrief
|
||||
} from '../types.js';
|
||||
import { ContextStore } from '../services/context-store.js';
|
||||
import { OAuthService } from '../services/oauth-service.js';
|
||||
@@ -18,6 +19,10 @@ import {
|
||||
type Brief,
|
||||
type RemoteTask
|
||||
} from '../services/organization.service.js';
|
||||
import {
|
||||
ERROR_CODES,
|
||||
TaskMasterError
|
||||
} from '../../../common/errors/task-master-error.js';
|
||||
import { getLogger } from '../../../common/logger/index.js';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
@@ -430,4 +435,28 @@ export class AuthManager {
|
||||
const service = await this.getOrganizationService();
|
||||
return service.getTasks(briefId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure a brief is selected in the current context
|
||||
* Throws a TaskMasterError if no brief is selected
|
||||
* @param operation - The operation name for error context
|
||||
* @returns The current user context with a guaranteed briefId
|
||||
*/
|
||||
ensureBriefSelected(operation: string): UserContextWithBrief {
|
||||
const context = this.getContext();
|
||||
|
||||
if (!context?.briefId) {
|
||||
throw new TaskMasterError(
|
||||
'No brief selected',
|
||||
ERROR_CODES.NO_BRIEF_SELECTED,
|
||||
{
|
||||
operation,
|
||||
userMessage:
|
||||
'No brief selected. Please select a brief first using: tm context brief <brief-id> or tm context brief <brief-url>'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return context as UserContextWithBrief;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,11 @@ export interface UserContext {
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* User context with a guaranteed briefId
|
||||
*/
|
||||
export type UserContextWithBrief = UserContext & { briefId: string };
|
||||
|
||||
export interface OAuthFlowOptions {
|
||||
/** Callback to open the browser with the auth URL. If not provided, browser won't be opened */
|
||||
openBrowser?: (url: string) => Promise<void>;
|
||||
|
||||
@@ -53,13 +53,6 @@ export interface ExpandTaskOptions {
|
||||
force?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Auth context with a guaranteed briefId
|
||||
*/
|
||||
type ContextWithBrief = NonNullable<
|
||||
ReturnType<typeof AuthManager.prototype.getContext>
|
||||
> & { briefId: string };
|
||||
|
||||
/**
|
||||
* TaskExpansionService handles AI-powered task expansion
|
||||
*/
|
||||
@@ -93,7 +86,7 @@ export class TaskExpansionService {
|
||||
): Promise<ExpandTaskResult> {
|
||||
try {
|
||||
// Get brief context from AuthManager
|
||||
const context = this.ensureBriefSelected('expandTask');
|
||||
const context = this.authManager.ensureBriefSelected('expandTask');
|
||||
|
||||
// Get the task being expanded to extract existing subtasks
|
||||
const task = await this.repository.getTask(this.projectId, taskId);
|
||||
@@ -101,7 +94,7 @@ export class TaskExpansionService {
|
||||
if (!task) {
|
||||
throw new TaskMasterError(
|
||||
`Task ${taskId} not found`,
|
||||
ERROR_CODES.VALIDATION_ERROR,
|
||||
ERROR_CODES.TASK_NOT_FOUND,
|
||||
{
|
||||
operation: 'expandTask',
|
||||
taskId,
|
||||
@@ -230,26 +223,4 @@ export class TaskExpansionService {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure a brief is selected in the current context
|
||||
* @returns The current auth context with a valid briefId
|
||||
*/
|
||||
private ensureBriefSelected(operation: string): ContextWithBrief {
|
||||
const context = this.authManager.getContext();
|
||||
|
||||
if (!context?.briefId) {
|
||||
throw new TaskMasterError(
|
||||
'No brief selected',
|
||||
ERROR_CODES.NO_BRIEF_SELECTED,
|
||||
{
|
||||
operation,
|
||||
userMessage:
|
||||
'No brief selected. Please select a brief first using: tm context brief <brief-id> or tm context brief <brief-url>'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return context as ContextWithBrief;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* @fileoverview Task Retrieval Service
|
||||
* Core service for retrieving tasks with enriched document content
|
||||
* Uses repository for task structure and API for document content
|
||||
*/
|
||||
|
||||
import {
|
||||
ERROR_CODES,
|
||||
TaskMasterError
|
||||
} from '../../../common/errors/task-master-error.js';
|
||||
import type { TaskRepository } from '../../tasks/repositories/task-repository.interface.js';
|
||||
import { AuthManager } from '../../auth/managers/auth-manager.js';
|
||||
import { ApiClient } from '../../storage/utils/api-client.js';
|
||||
import { getLogger } from '../../../common/logger/factory.js';
|
||||
import type { Task } from '../../../common/types/index.js';
|
||||
|
||||
/**
|
||||
* Response from the get task API endpoint
|
||||
*/
|
||||
interface GetTaskResponse {
|
||||
task: Task;
|
||||
document?: {
|
||||
id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* TaskRetrievalService handles fetching tasks with enriched document content
|
||||
* Uses repository for task structure and API endpoint for document content
|
||||
*/
|
||||
export class TaskRetrievalService {
|
||||
private readonly repository: TaskRepository;
|
||||
private readonly projectId: string;
|
||||
private readonly apiClient: ApiClient;
|
||||
private readonly authManager: AuthManager;
|
||||
private readonly logger = getLogger('TaskRetrievalService');
|
||||
|
||||
constructor(
|
||||
repository: TaskRepository,
|
||||
projectId: string,
|
||||
apiClient: ApiClient,
|
||||
authManager: AuthManager
|
||||
) {
|
||||
this.repository = repository;
|
||||
this.projectId = projectId;
|
||||
this.apiClient = apiClient;
|
||||
this.authManager = authManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get task by ID (UUID or display ID like HAM-123)
|
||||
* Uses repository for task structure and API for enriched document content
|
||||
* @returns Task with subtasks, dependencies, and document content in details field
|
||||
*/
|
||||
async getTask(taskId: string): Promise<Task | null> {
|
||||
try {
|
||||
this.authManager.ensureBriefSelected('getTask');
|
||||
|
||||
const task = await this.repository.getTask(this.projectId, taskId);
|
||||
|
||||
if (!task) {
|
||||
throw new TaskMasterError(
|
||||
`Task ${taskId} not found`,
|
||||
ERROR_CODES.TASK_NOT_FOUND,
|
||||
{
|
||||
operation: 'getTask',
|
||||
taskId,
|
||||
userMessage: `Task ${taskId} isn't available in the current project.`
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Fetch document content from API and merge into task.details
|
||||
try {
|
||||
const url = `/ai/api/v1/tasks/${task.id}`;
|
||||
const apiResult = await this.apiClient.get<GetTaskResponse>(url);
|
||||
|
||||
if (apiResult.document?.content) {
|
||||
task.details = apiResult.document.content;
|
||||
}
|
||||
} catch (error) {
|
||||
// Document fetch failed - log but don't fail the whole operation
|
||||
this.logger.debug(
|
||||
`Could not fetch document content for task ${taskId}: ${error}`
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.info(`✓ Retrieved task ${taskId}`);
|
||||
if (task.details) {
|
||||
this.logger.debug(
|
||||
` Document content available (${task.details.length} chars)`
|
||||
);
|
||||
}
|
||||
|
||||
return task;
|
||||
} catch (error) {
|
||||
// If it's already a TaskMasterError, just add context and re-throw
|
||||
if (error instanceof TaskMasterError) {
|
||||
throw error.withContext({
|
||||
operation: 'getTask',
|
||||
taskId
|
||||
});
|
||||
}
|
||||
|
||||
// For other errors, wrap them
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
throw new TaskMasterError(
|
||||
errorMessage,
|
||||
ERROR_CODES.STORAGE_ERROR,
|
||||
{
|
||||
operation: 'getTask',
|
||||
taskId
|
||||
},
|
||||
error as Error
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
ExpandTaskResult,
|
||||
TaskExpansionService
|
||||
} from '../../integration/services/task-expansion.service.js';
|
||||
import { TaskRetrievalService } from '../../integration/services/task-retrieval.service.js';
|
||||
|
||||
/**
|
||||
* API storage configuration
|
||||
@@ -46,13 +47,6 @@ export interface ApiStorageConfig {
|
||||
maxRetries?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Auth context with a guaranteed briefId
|
||||
*/
|
||||
type ContextWithBrief = NonNullable<
|
||||
ReturnType<typeof AuthManager.prototype.getContext>
|
||||
> & { briefId: string };
|
||||
|
||||
/**
|
||||
* Response from the update task with prompt API endpoint
|
||||
*/
|
||||
@@ -82,6 +76,7 @@ export class ApiStorage implements IStorage {
|
||||
private tagsCache: Map<string, TaskTag> = new Map();
|
||||
private apiClient?: ApiClient;
|
||||
private expansionService?: TaskExpansionService;
|
||||
private retrievalService?: TaskRetrievalService;
|
||||
private readonly logger = getLogger('ApiStorage');
|
||||
|
||||
constructor(config: ApiStorageConfig) {
|
||||
@@ -203,7 +198,8 @@ export class ApiStorage implements IStorage {
|
||||
await this.ensureInitialized();
|
||||
|
||||
try {
|
||||
const context = this.ensureBriefSelected('loadTasks');
|
||||
const context =
|
||||
AuthManager.getInstance().ensureBriefSelected('loadTasks');
|
||||
|
||||
// Load tasks from the current brief context with filters pushed to repository
|
||||
const tasks = await this.retryOperation(() =>
|
||||
@@ -267,17 +263,14 @@ export class ApiStorage implements IStorage {
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a single task by ID
|
||||
* Load a single task by ID (supports UUID or display ID like HAM-123)
|
||||
*/
|
||||
async loadTask(taskId: string, tag?: string): Promise<Task | null> {
|
||||
await this.ensureInitialized();
|
||||
|
||||
try {
|
||||
this.ensureBriefSelected('loadTask');
|
||||
|
||||
return await this.retryOperation(() =>
|
||||
this.repository.getTask(this.projectId, taskId)
|
||||
);
|
||||
const retrievalService = this.getRetrievalService();
|
||||
return await this.retryOperation(() => retrievalService.getTask(taskId));
|
||||
} catch (error) {
|
||||
this.wrapError(error, 'Failed to load task from API', {
|
||||
operation: 'loadTask',
|
||||
@@ -637,7 +630,7 @@ export class ApiStorage implements IStorage {
|
||||
await this.ensureInitialized();
|
||||
|
||||
try {
|
||||
this.ensureBriefSelected('updateTaskStatus');
|
||||
AuthManager.getInstance().ensureBriefSelected('updateTaskStatus');
|
||||
|
||||
const existingTask = await this.retryOperation(() =>
|
||||
this.repository.getTask(this.projectId, taskId)
|
||||
@@ -898,29 +891,6 @@ export class ApiStorage implements IStorage {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure a brief is selected in the current context
|
||||
* @returns The current auth context with a valid briefId
|
||||
*/
|
||||
private ensureBriefSelected(operation: string): ContextWithBrief {
|
||||
const authManager = AuthManager.getInstance();
|
||||
const context = authManager.getContext();
|
||||
|
||||
if (!context?.briefId) {
|
||||
throw new TaskMasterError(
|
||||
'No brief selected',
|
||||
ERROR_CODES.NO_BRIEF_SELECTED,
|
||||
{
|
||||
operation,
|
||||
userMessage:
|
||||
'No brief selected. Please select a brief first using: tm context brief <brief-id> or tm context brief <brief-url>'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return context as ContextWithBrief;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create API client instance with auth
|
||||
*/
|
||||
@@ -937,7 +907,8 @@ export class ApiStorage implements IStorage {
|
||||
);
|
||||
}
|
||||
|
||||
const context = this.ensureBriefSelected('getApiClient');
|
||||
const context =
|
||||
AuthManager.getInstance().ensureBriefSelected('getApiClient');
|
||||
const authManager = AuthManager.getInstance();
|
||||
|
||||
this.apiClient = new ApiClient({
|
||||
@@ -969,6 +940,25 @@ export class ApiStorage implements IStorage {
|
||||
return this.expansionService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create TaskRetrievalService instance
|
||||
*/
|
||||
private getRetrievalService(): TaskRetrievalService {
|
||||
if (!this.retrievalService) {
|
||||
const apiClient = this.getApiClient();
|
||||
const authManager = AuthManager.getInstance();
|
||||
|
||||
this.retrievalService = new TaskRetrievalService(
|
||||
this.repository,
|
||||
this.projectId,
|
||||
apiClient,
|
||||
authManager
|
||||
);
|
||||
}
|
||||
|
||||
return this.retrievalService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retry an operation with exponential backoff
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user