Skip to main content
Glama
plan-service.ts10.6 kB
import { v4 as uuidv4 } from 'uuid'; import type { FileStorage } from '../../infrastructure/file-storage.js'; import type { PlanManifest, Plan, PlanStatus, Requirement, Solution, Decision, Phase, Artifact, Link, } from '../entities/types.js'; // Input types export interface CreatePlanInput { name: string; description: string; author?: string; } export interface ListPlansInput { status?: PlanStatus; limit?: number; offset?: number; sortBy?: 'created_at' | 'updated_at' | 'name'; sortOrder?: 'asc' | 'desc'; } export interface GetPlanInput { planId: string; includeEntities?: boolean; entityTypes?: ('requirement' | 'solution' | 'decision' | 'phase' | 'artifact')[]; } export interface UpdatePlanInput { planId: string; updates: { name?: string; description?: string; status?: PlanStatus; }; } export interface ArchivePlanInput { planId: string; permanent?: boolean; } export interface SetActivePlanInput { planId: string; workspacePath?: string; } export interface GetActivePlanInput { workspacePath?: string; } // Output types export interface CreatePlanResult { planId: string; manifest: PlanManifest; createdAt: string; } export interface ListPlansResult { plans: Array<{ id: string; name: string; description: string; status: PlanStatus; createdAt: string; updatedAt: string; statistics: PlanManifest['statistics']; }>; total: number; hasMore: boolean; } export interface GetPlanResult { plan: { manifest: PlanManifest; entities?: { requirements: Requirement[]; solutions: Solution[]; decisions: Decision[]; phases: Phase[]; artifacts: Artifact[]; }; links?: Link[]; }; } export interface UpdatePlanResult { success: boolean; plan: PlanManifest; updatedAt: string; } export interface ArchivePlanResult { success: boolean; message: string; } export interface SetActivePlanResult { success: boolean; activePlan: { planId: string; planName: string; workspacePath: string; }; } export interface GetActivePlanResult { activePlan: { planId: string; plan: PlanManifest; lastUpdated: string; } | null; } export class PlanService { constructor(private storage: FileStorage) {} async createPlan(input: CreatePlanInput): Promise<CreatePlanResult> { const planId = uuidv4(); const now = new Date().toISOString(); const manifest: PlanManifest = { id: planId, name: input.name, description: input.description, status: 'active', author: input.author || 'claude-code', createdAt: now, updatedAt: now, version: 1, lockVersion: 1, statistics: { totalRequirements: 0, totalSolutions: 0, totalDecisions: 0, totalPhases: 0, totalArtifacts: 0, completionPercentage: 0, }, }; await this.storage.createPlanDirectory(planId); await this.storage.saveManifest(planId, manifest); // Initialize empty entity files await this.storage.saveEntities(planId, 'requirements', []); await this.storage.saveEntities(planId, 'solutions', []); await this.storage.saveEntities(planId, 'decisions', []); await this.storage.saveEntities(planId, 'phases', []); await this.storage.saveEntities(planId, 'artifacts', []); await this.storage.saveLinks(planId, []); return { planId, manifest, createdAt: now, }; } async listPlans(input: ListPlansInput): Promise<ListPlansResult> { const planIds = await this.storage.listPlans(); const manifests: PlanManifest[] = []; for (const planId of planIds) { try { const manifest = await this.storage.loadManifest(planId); manifests.push(manifest); } catch { // Skip invalid plans } } // Filter by status let filtered = input.status ? manifests.filter((m) => m.status === input.status) : manifests; // Sort const sortBy = input.sortBy || 'updated_at'; const sortOrder = input.sortOrder || 'desc'; filtered.sort((a, b) => { let valueA: string | number; let valueB: string | number; if (sortBy === 'name') { valueA = a.name.toLowerCase(); valueB = b.name.toLowerCase(); } else if (sortBy === 'created_at') { valueA = a.createdAt; valueB = b.createdAt; } else { valueA = a.updatedAt; valueB = b.updatedAt; } if (valueA < valueB) return sortOrder === 'asc' ? -1 : 1; if (valueA > valueB) return sortOrder === 'asc' ? 1 : -1; return 0; }); // Pagination const total = filtered.length; const offset = input.offset || 0; const limit = input.limit || 50; const paginated = filtered.slice(offset, offset + limit); return { plans: paginated.map((m) => ({ id: m.id, name: m.name, description: m.description, status: m.status, createdAt: m.createdAt, updatedAt: m.updatedAt, statistics: m.statistics, })), total, hasMore: offset + limit < total, }; } async getPlan(input: GetPlanInput): Promise<GetPlanResult> { const exists = await this.storage.planExists(input.planId); if (!exists) { throw new Error('Plan not found'); } const manifest = await this.storage.loadManifest(input.planId); const result: GetPlanResult = { plan: { manifest }, }; if (input.includeEntities) { result.plan.entities = { requirements: await this.storage.loadEntities<Requirement>( input.planId, 'requirements' ), solutions: await this.storage.loadEntities<Solution>( input.planId, 'solutions' ), decisions: await this.storage.loadEntities<Decision>( input.planId, 'decisions' ), phases: await this.storage.loadEntities<Phase>(input.planId, 'phases'), artifacts: await this.storage.loadEntities<Artifact>(input.planId, 'artifacts'), }; result.plan.links = await this.storage.loadLinks(input.planId); } return result; } async updatePlan(input: UpdatePlanInput): Promise<UpdatePlanResult> { const exists = await this.storage.planExists(input.planId); if (!exists) { throw new Error('Plan not found'); } const manifest = await this.storage.loadManifest(input.planId); const now = new Date().toISOString(); // Apply updates if (input.updates.name !== undefined) { manifest.name = input.updates.name; } if (input.updates.description !== undefined) { manifest.description = input.updates.description; } if (input.updates.status !== undefined) { manifest.status = input.updates.status; } manifest.updatedAt = now; manifest.version += 1; await this.storage.saveManifest(input.planId, manifest); return { success: true, plan: manifest, updatedAt: now, }; } async archivePlan(input: ArchivePlanInput): Promise<ArchivePlanResult> { const exists = await this.storage.planExists(input.planId); if (!exists) { throw new Error('Plan not found'); } if (input.permanent) { await this.storage.deletePlan(input.planId); return { success: true, message: 'Plan permanently deleted', }; } // Soft delete - just change status await this.updatePlan({ planId: input.planId, updates: { status: 'archived' }, }); return { success: true, message: 'Plan archived', }; } async setActivePlan(input: SetActivePlanInput): Promise<SetActivePlanResult> { const exists = await this.storage.planExists(input.planId); if (!exists) { throw new Error('Plan not found'); } const manifest = await this.storage.loadManifest(input.planId); const workspacePath = input.workspacePath || process.cwd(); const now = new Date().toISOString(); const activePlans = await this.storage.loadActivePlans(); activePlans[workspacePath] = { planId: input.planId, lastUpdated: now, }; await this.storage.saveActivePlans(activePlans); return { success: true, activePlan: { planId: input.planId, planName: manifest.name, workspacePath, }, }; } async getActivePlan(input: GetActivePlanInput): Promise<GetActivePlanResult> { const workspacePath = input.workspacePath || process.cwd(); const activePlans = await this.storage.loadActivePlans(); const mapping = activePlans[workspacePath]; if (!mapping) { return { activePlan: null }; } try { const manifest = await this.storage.loadManifest(mapping.planId); return { activePlan: { planId: mapping.planId, plan: manifest, lastUpdated: mapping.lastUpdated, }, }; } catch { // Plan was deleted, clear mapping delete activePlans[workspacePath]; await this.storage.saveActivePlans(activePlans); return { activePlan: null }; } } // Helper to update statistics async updateStatistics(planId: string): Promise<void> { const manifest = await this.storage.loadManifest(planId); const requirements = await this.storage.loadEntities<Requirement>( planId, 'requirements' ); const solutions = await this.storage.loadEntities<Solution>( planId, 'solutions' ); const decisions = await this.storage.loadEntities<Decision>( planId, 'decisions' ); const phases = await this.storage.loadEntities<Phase>(planId, 'phases'); const artifacts = await this.storage.loadEntities<Artifact>(planId, 'artifacts'); manifest.statistics.totalRequirements = requirements.length; manifest.statistics.totalSolutions = solutions.length; manifest.statistics.totalDecisions = decisions.length; manifest.statistics.totalPhases = phases.length; manifest.statistics.totalArtifacts = artifacts.length; // Calculate completion percentage if (phases.length > 0) { const completedPhases = phases.filter( (p) => p.status === 'completed' ).length; manifest.statistics.completionPercentage = Math.round( (completedPhases / phases.length) * 100 ); } manifest.updatedAt = new Date().toISOString(); await this.storage.saveManifest(planId, manifest); } } export default PlanService;

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/cppmyjob/cpp-mcp-planner'

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