Skip to main content
Glama
brief-selection.ts7.94 kB
/** * @fileoverview Shared brief selection utilities * Reusable functions for selecting briefs interactively or via URL/ID */ import search from '@inquirer/search'; import type { AuthManager } from '@tm/core'; import { formatRelativeTime } from '@tm/core'; import chalk from 'chalk'; import ora, { type Ora } from 'ora'; import { getBriefStatusWithColor } from '../ui/formatters/status-formatters.js'; import * as ui from './ui.js'; export interface BriefSelectionResult { success: boolean; briefId?: string; briefName?: string; orgId?: string; orgName?: string; message?: string; } /** * Select a brief interactively using search */ export async function selectBriefInteractive( authManager: AuthManager, orgId: string ): Promise<BriefSelectionResult> { const spinner = ora('Fetching briefs...').start(); try { // Fetch briefs from API const briefs = await authManager.getBriefs(orgId); spinner.stop(); if (briefs.length === 0) { ui.displayWarning('No briefs available in this organization'); return { success: false, message: 'No briefs available' }; } // Prompt for selection with search const selectedBrief = await search<(typeof briefs)[0] | null>({ message: 'Search for a brief:', pageSize: 15, source: async (input) => { const searchTerm = input?.toLowerCase() || ''; // Static option for no brief const noBriefOption = { name: '(No brief - organization level)', value: null as any, description: 'Clear brief selection' }; // Filter briefs based on search term const filteredBriefs = briefs.filter((brief) => { if (!searchTerm) return true; const title = brief.document?.title || ''; const shortId = brief.id.slice(0, 8); const lastChars = brief.id.slice(-8); // Search by title, full UUID, first 8 chars, or last 8 chars return ( title.toLowerCase().includes(searchTerm) || brief.id.toLowerCase().includes(searchTerm) || shortId.toLowerCase().includes(searchTerm) || lastChars.toLowerCase().includes(searchTerm) ); }); // Group briefs by status const briefsByStatus = filteredBriefs.reduce( (acc, brief) => { const status = brief.status || 'unknown'; if (!acc[status]) { acc[status] = []; } acc[status].push(brief); return acc; }, {} as Record<string, typeof briefs> ); // Define status order (most active first) const statusOrder = [ 'delivering', 'aligned', 'refining', 'draft', 'delivered', 'done', 'archived' ]; // Build grouped options const groupedOptions: any[] = []; for (const status of statusOrder) { const statusBriefs = briefsByStatus[status]; if (!statusBriefs || statusBriefs.length === 0) continue; // Add status header as separator const statusHeader = getBriefStatusWithColor(status); groupedOptions.push({ type: 'separator', separator: `\n${statusHeader}` }); // Add briefs under this status statusBriefs.forEach((brief) => { const title = brief.document?.title || `Brief ${brief.id.slice(-8)}`; const shortId = brief.id.slice(-8); const description = brief.document?.description || ''; const taskCountDisplay = brief.taskCount !== undefined && brief.taskCount > 0 ? chalk.gray( ` (${brief.taskCount} ${brief.taskCount === 1 ? 'task' : 'tasks'})` ) : ''; const updatedAtDisplay = brief.updatedAt ? chalk.gray(` • ${formatRelativeTime(brief.updatedAt)}`) : ''; groupedOptions.push({ name: ` ${title}${taskCountDisplay} ${chalk.gray(`(${shortId})`)}${updatedAtDisplay}`, value: brief, description: description ? chalk.gray(` ${description.slice(0, 80)}`) : undefined }); }); } // Handle any briefs with statuses not in our order const unorderedStatuses = Object.keys(briefsByStatus).filter( (s) => !statusOrder.includes(s) ); for (const status of unorderedStatuses) { const statusBriefs = briefsByStatus[status]; if (!statusBriefs || statusBriefs.length === 0) continue; const statusHeader = getBriefStatusWithColor(status); groupedOptions.push({ type: 'separator', separator: `\n${statusHeader}` }); statusBriefs.forEach((brief) => { const title = brief.document?.title || `Brief ${brief.id.slice(-8)}`; const shortId = brief.id.slice(-8); const description = brief.document?.description || ''; const taskCountDisplay = brief.taskCount !== undefined && brief.taskCount > 0 ? chalk.gray( ` (${brief.taskCount} ${brief.taskCount === 1 ? 'task' : 'tasks'})` ) : ''; const updatedAtDisplay = brief.updatedAt ? chalk.gray(` • ${formatRelativeTime(brief.updatedAt)}`) : ''; groupedOptions.push({ name: ` ${title}${taskCountDisplay} ${chalk.gray(`(${shortId})`)}${updatedAtDisplay}`, value: brief, description: description ? chalk.gray(` ${description.slice(0, 80)}`) : undefined }); }); } return [noBriefOption, ...groupedOptions]; } }); if (selectedBrief) { // Update context with brief const briefName = selectedBrief.document?.title || `Brief ${selectedBrief.id.slice(0, 8)}`; await authManager.updateContext({ briefId: selectedBrief.id, briefName: briefName, briefStatus: selectedBrief.status, briefUpdatedAt: selectedBrief.updatedAt }); ui.displaySuccess(`Selected brief: ${briefName}`); return { success: true, briefId: selectedBrief.id, briefName, message: `Selected brief: ${briefName}` }; } else { // Clear brief selection await authManager.updateContext({ briefId: undefined, briefName: undefined, briefStatus: undefined, briefUpdatedAt: undefined }); ui.displaySuccess('Cleared brief selection (organization level)'); return { success: true, message: 'Cleared brief selection' }; } } catch (error) { spinner.fail('Failed to fetch briefs'); throw error; } } /** * Select a brief from any input format (URL, ID, name) using tm-core * Presentation layer - handles display and context updates only * * All business logic (URL parsing, ID matching, name resolution) is in tm-core */ export async function selectBriefFromInput( authManager: AuthManager, input: string, tmCore: any ): Promise<BriefSelectionResult> { let spinner: Ora | undefined; try { spinner = ora('Resolving brief...'); spinner.start(); // Let tm-core handle ALL business logic: // - URL parsing // - ID extraction // - UUID matching (full or last 8 chars) // - Name matching const brief = await tmCore.tasks.resolveBrief(input); // Fetch org to get a friendly name and slug (optional) let orgName: string | undefined; let orgSlug: string | undefined; try { const org = await authManager.getOrganization(brief.accountId); orgName = org?.name; orgSlug = org?.slug; } catch { // Non-fatal if org lookup fails } // Update context: set org and brief const briefName = brief.document?.title || `Brief ${brief.id.slice(0, 8)}`; await authManager.updateContext({ orgId: brief.accountId, orgName, orgSlug, briefId: brief.id, briefName, briefStatus: brief.status, briefUpdatedAt: brief.updatedAt }); spinner.succeed('Context set from brief'); console.log( chalk.gray( ` Organization: ${orgName || brief.accountId}\n Brief: ${briefName}` ) ); return { success: true, briefId: brief.id, briefName, orgId: brief.accountId, orgName, message: 'Context set from brief' }; } catch (error) { try { if (spinner?.isSpinning) spinner.stop(); } catch {} throw error; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/eyaltoledano/claude-task-master'

If you have feedback or need assistance with the MCP directory API, please join our Discord server