Skip to main content
Glama

COA Goldfish MCP

by anortham
index-manager.tsโ€ข11.9 kB
/** * Index Manager - Manages relationship mappings and metadata * Provides efficient lookups and relationship tracking between plans, todos, and checkpoints */ import fs from 'fs-extra'; import { join } from 'path'; import { Storage } from './storage.js'; import { GoldfishMemory, Plan } from '../types/index.js'; export interface RelationshipIndex { version: string; lastUpdated: string; workspace: string; relationships: RelationshipEntry[]; metadata: IndexMetadata; } export interface RelationshipEntry { planId?: string; planTitle?: string; planStatus?: 'draft' | 'active' | 'completed' | 'abandoned'; linkedTodos: string[]; linkedCheckpoints: string[]; createdAt: string; updatedAt: string; completionPercentage?: number; tags: string[]; } export interface IndexMetadata { totalPlans: number; totalTodos: number; totalCheckpoints: number; activePlans: number; completedTasks: number; lastStandupGenerated?: string; configVersion: string; } export class IndexManager { private storage: Storage; private indexPath: string; constructor(storage: Storage, workspace?: string) { this.storage = storage; const targetWorkspace = workspace || storage.getCurrentWorkspace(); this.indexPath = join(storage.getWorkspaceDir(targetWorkspace), 'index.json'); } /** * Load the relationship index from file */ async loadIndex(): Promise<RelationshipIndex> { try { if (await fs.pathExists(this.indexPath)) { const data = await fs.readJson(this.indexPath); return data as RelationshipIndex; } } catch (error) { console.error('Failed to load relationship index:', error); } // Return default index if file doesn't exist or is corrupted return this.createDefaultIndex(); } /** * Save the relationship index to file */ async saveIndex(index: RelationshipIndex): Promise<void> { try { await fs.ensureDir(join(this.indexPath, '..')); await fs.writeJson(this.indexPath, index, { spaces: 2 }); } catch (error) { console.error('Failed to save relationship index:', error); throw error; } } /** * Update the index with new relationship data */ async updateRelationships(): Promise<RelationshipIndex> { const index = await this.loadIndex(); // Load all memories for this workspace const memories = await this.storage.loadAllMemories(); // Reset relationships and rebuild index.relationships = []; // Process plans and build relationships const plans = memories.filter(m => m.type === 'plan'); const todos = memories.filter(m => m.type === 'todo'); const checkpoints = memories.filter(m => m.type === 'checkpoint'); for (const planMemory of plans) { if (typeof planMemory.content === 'object' && planMemory.content) { const plan = planMemory.content as Plan; // Calculate dynamic completion percentage based on current TODO state let completionPercentage = 0; if (plan.generatedTodos && plan.generatedTodos.length > 0) { let totalTasks = 0; let completedTasks = 0; for (const todoId of plan.generatedTodos) { const todoMemory = todos.find(t => { if (typeof t.content === 'object' && t.content) { return (t.content as any).id === todoId; } return false; }); if (todoMemory && typeof todoMemory.content === 'object' && todoMemory.content) { const todoList = todoMemory.content as any; if (todoList.items && Array.isArray(todoList.items)) { totalTasks += todoList.items.length; completedTasks += todoList.items.filter((item: any) => item.status === 'done').length; } } } completionPercentage = totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0; } const relationship: RelationshipEntry = { planId: plan.id, planTitle: plan.title, planStatus: plan.status === 'complete' ? 'completed' : plan.status, linkedTodos: [], linkedCheckpoints: [], createdAt: plan.createdAt instanceof Date ? plan.createdAt.toISOString() : plan.createdAt, updatedAt: plan.updatedAt instanceof Date ? plan.updatedAt.toISOString() : plan.updatedAt, completionPercentage: completionPercentage, tags: plan.tags || [] }; // Find linked TODOs if (Array.isArray(plan.generatedTodos)) { relationship.linkedTodos.push(...plan.generatedTodos); } // Find linked checkpoints if (Array.isArray(plan.relatedCheckpoints)) { relationship.linkedCheckpoints.push(...plan.relatedCheckpoints); } // Search for additional relationships by content matching for (const checkpoint of checkpoints) { if (typeof checkpoint.content === 'object' && checkpoint.content) { const content = checkpoint.content as { description?: string; planId?: string; highlights?: string[] }; let isRelated = false; // Direct planId reference if (content.planId === plan.id) { isRelated = true; } // Plan title mentioned in checkpoint description if (content.description && content.description.includes(plan.title)) { isRelated = true; } // Keyword-based matching using plan tags if (plan.tags && plan.tags.length > 0) { const checkpointText = [ content.description || '', ...(content.highlights || []) ].join(' ').toLowerCase(); for (const tag of plan.tags) { if (checkpointText.includes(tag.toLowerCase())) { isRelated = true; break; } } } if (isRelated && !relationship.linkedCheckpoints.includes(checkpoint.id)) { relationship.linkedCheckpoints.push(checkpoint.id); } } } // Search for TODOs that mention the plan for (const todo of todos) { if (typeof todo.content === 'object' && todo.content) { const content = todo.content as { title?: string; planId?: string }; if (content.planId === plan.id || (content.title && content.title.includes(plan.title))) { if (!relationship.linkedTodos.includes(todo.id)) { relationship.linkedTodos.push(todo.id); } } } } index.relationships.push(relationship); } } // Update metadata index.metadata = { totalPlans: plans.length, totalTodos: todos.length, totalCheckpoints: checkpoints.length, activePlans: plans.filter(p => { const plan = p.content as Plan; return plan.status === 'active' || plan.status === 'draft'; }).length, completedTasks: todos.reduce((sum, todoMemory) => { if (typeof todoMemory.content === 'object' && todoMemory.content) { const content = todoMemory.content as { items?: any[] }; if (Array.isArray(content.items)) { return sum + content.items.filter((item: any) => item.status === 'done').length; } } return sum; }, 0), lastStandupGenerated: index.metadata.lastStandupGenerated, configVersion: '2.0.0' }; index.lastUpdated = new Date().toISOString(); await this.saveIndex(index); return index; } /** * Add or update a relationship entry */ async updateRelationship(planId: string, updates: Partial<RelationshipEntry>): Promise<void> { const index = await this.loadIndex(); const existingIndex = index.relationships.findIndex(r => r.planId === planId); if (existingIndex >= 0) { // Update existing relationship const updatedEntry = { ...index.relationships[existingIndex], ...updates, updatedAt: new Date().toISOString() }; // Ensure required arrays are present if (!updatedEntry.linkedTodos) updatedEntry.linkedTodos = []; if (!updatedEntry.linkedCheckpoints) updatedEntry.linkedCheckpoints = []; index.relationships[existingIndex] = updatedEntry as RelationshipEntry; } else { // Create new relationship entry const newRelationship: RelationshipEntry = { planId, linkedTodos: [], linkedCheckpoints: [], createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), tags: [], ...updates }; // Ensure required arrays are present if (!newRelationship.linkedTodos) newRelationship.linkedTodos = []; if (!newRelationship.linkedCheckpoints) newRelationship.linkedCheckpoints = []; index.relationships.push(newRelationship); } await this.saveIndex(index); } /** * Remove a relationship entry */ async removeRelationship(planId: string): Promise<void> { const index = await this.loadIndex(); index.relationships = index.relationships.filter(r => r.planId !== planId); await this.saveIndex(index); } /** * Get relationships for a specific plan */ async getRelationshipsForPlan(planId: string): Promise<RelationshipEntry | null> { const index = await this.loadIndex(); return index.relationships.find(r => r.planId === planId) || null; } /** * Get all active relationships */ async getActiveRelationships(): Promise<RelationshipEntry[]> { const index = await this.loadIndex(); return index.relationships.filter(r => r.planStatus === 'active' || r.planStatus === 'draft' ); } /** * Record that a standup was generated */ async recordStandupGeneration(): Promise<void> { const index = await this.loadIndex(); index.metadata.lastStandupGenerated = new Date().toISOString(); await this.saveIndex(index); } /** * Get index statistics */ async getStatistics(): Promise<IndexMetadata> { const index = await this.loadIndex(); return index.metadata; } /** * Create default index structure */ private createDefaultIndex(): RelationshipIndex { return { version: '2.0.0', lastUpdated: new Date().toISOString(), workspace: this.storage.getCurrentWorkspace(), relationships: [], metadata: { totalPlans: 0, totalTodos: 0, totalCheckpoints: 0, activePlans: 0, completedTasks: 0, configVersion: '2.0.0' } }; } /** * Repair/rebuild the entire index from scratch */ async rebuildIndex(): Promise<RelationshipIndex> { // Create fresh index const newIndex = this.createDefaultIndex(); await this.saveIndex(newIndex); // Rebuild relationships return await this.updateRelationships(); } /** * Clean up orphaned relationships (plans that no longer exist) */ async cleanupOrphanedRelationships(): Promise<void> { const index = await this.loadIndex(); const memories = await this.storage.loadAllMemories(); const existingPlanIds = memories .filter(m => m.type === 'plan') .map(m => { if (typeof m.content === 'object' && m.content) { return (m.content as Plan).id; } return null; }) .filter(Boolean) as string[]; // Remove relationships for plans that no longer exist index.relationships = index.relationships.filter(r => r.planId && existingPlanIds.includes(r.planId) ); await this.saveIndex(index); } }

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/anortham/coa-goldfish-mcp'

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