fix: get tasks (#1385)

* fix: get tasks by calling an endpoint

* chore: apply requested changes
This commit is contained in:
Ralph Khreish
2025-11-07 21:26:55 +01:00
parent 3d7e77c178
commit 04e752704e
5 changed files with 189 additions and 71 deletions

View File

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

View File

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

View File

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

View File

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

View File

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