UNPKG

claude-agents-manager

Version:

Elite AI research and development platform with 60+ specialized agents, comprehensive research workflows, citation-backed reports, and advanced multi-agent coordination for Claude Code. Features deep research capabilities, concurrent execution, shared mem

664 lines (570 loc) 16.3 kB
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs"; import { dirname, join } from "path"; import { fileURLToPath } from "url"; import { detectContextForge, createContextAwareConfig, } from "../utils/contextForgeDetector.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); /** * Simple memory store with JSON persistence * Provides shared memory for agent coordination * Enhanced with context-forge awareness */ class SimpleMemoryStore { constructor(optionsOrPath = {}) { // Support passing a file path directly for testing if (typeof optionsOrPath === "string") { this.memoryFile = optionsOrPath; this.memoryDir = dirname(optionsOrPath); } else { this.memoryDir = optionsOrPath.memoryDir || join(process.cwd(), ".swarm"); this.memoryFile = join(this.memoryDir, "memory.json"); } this.store = new Map(); this.ttlTimers = new Map(); // Ensure directory exists if (!existsSync(this.memoryDir)) { mkdirSync(this.memoryDir, { recursive: true }); } // Load existing memory this.load(); // Initialize context-forge awareness this.initializeContextForge(); // Auto-save every 30 seconds (skip in test environment) if (process.env.NODE_ENV !== "test") { this.autoSaveInterval = setInterval(() => this.save(), 30000); } } /** * Initialize context-forge awareness * Stores project configuration and state in memory for agent coordination */ initializeContextForge() { try { const contextConfig = createContextAwareConfig(process.cwd()); if (contextConfig.isContextForgeProject) { // Store detection flag this.set("context-forge:detected", true, null); // Store project configuration this.set( "context-forge:config", { projectPath: contextConfig.projectPath, structure: contextConfig.detection.structure, techStack: contextConfig.detection.techStack, features: contextConfig.detection.features, }, null, ); // No TTL - permanent during session // Store project rules if available if (contextConfig.projectRules) { this.set("context-forge:rules", contextConfig.projectRules, null); } // Store available PRPs if ( contextConfig.availablePRPs && contextConfig.availablePRPs.length > 0 ) { this.set("context-forge:prps", contextConfig.availablePRPs, 3600000); // 1 hour TTL // Store individual PRP states contextConfig.availablePRPs.forEach((prp) => { this.set( `context-forge:prp:${prp.filename}:state`, { name: prp.name, goal: prp.goal, executed: false, validationPassed: false, lastAccessed: Date.now(), }, 3600000, ); }); } // Store implementation progress if (contextConfig.implementationPlan) { this.set( "context-forge:progress", contextConfig.implementationPlan, 3600000, ); // Store stage-specific progress contextConfig.implementationPlan.stages.forEach((stage) => { this.set( `context-forge:stage:${stage.number}`, { name: stage.name, progress: (stage.completedTasks / stage.totalTasks) * 100, completedTasks: stage.completedTasks, totalTasks: stage.totalTasks, }, 3600000, ); }); } // Store available commands if ( contextConfig.availableCommands && contextConfig.availableCommands.length > 0 ) { this.set( "context-forge:commands", contextConfig.availableCommands.map((cmd) => ({ name: cmd.name, category: cmd.category, description: cmd.description, })), 3600000, ); } console.log("Context-forge project detected and initialized in memory"); } } catch (error) { // Silently fail if context-forge detection has issues // This ensures the memory system works even without context-forge } } /** * Set a value in memory with optional TTL * @param {string} key - Namespaced key (e.g., "agent:planner:tasks") * @param {any} value - Value to store * @param {number|null} ttl - Time to live in milliseconds */ set(key, value, ttl = null) { // Clear existing TTL timer if present if (this.ttlTimers.has(key)) { clearTimeout(this.ttlTimers.get(key)); this.ttlTimers.delete(key); } // Store the value with metadata this.store.set(key, { value, created: Date.now(), expires: ttl ? Date.now() + ttl : null, accessed: Date.now(), accessCount: 1, }); // Set TTL timer if needed if (ttl) { const timer = setTimeout(() => { this.delete(key); }, ttl); this.ttlTimers.set(key, timer); } // Save to disk this.save(); return value; } /** * Get a value from memory * @param {string} key - Key to retrieve * @returns {any|null} - Stored value or null if not found/expired */ get(key) { const item = this.store.get(key); if (!item) return null; // Check if expired if (item.expires && Date.now() > item.expires) { this.delete(key); return null; } // Update access metadata item.accessed = Date.now(); item.accessCount++; return item.value; } /** * Delete a key from memory * @param {string} key - Key to delete */ delete(key) { // Clear TTL timer if present if (this.ttlTimers.has(key)) { clearTimeout(this.ttlTimers.get(key)); this.ttlTimers.delete(key); } const result = this.store.delete(key); if (result) { this.save(); } return result; } /** * Check if a key exists * @param {string} key - Key to check * @returns {boolean} */ has(key) { const item = this.store.get(key); if (!item) return false; // Check if expired if (item.expires && Date.now() > item.expires) { this.delete(key); return false; } return true; } /** * Get all keys matching a pattern * @param {string} pattern - Pattern to match (e.g., "agent:planner:*") * @returns {string[]} - Matching keys */ keys(pattern = "*") { const regex = new RegExp("^" + pattern.replace(/\*/g, ".*") + "$"); const validKeys = []; for (const [key, item] of this.store.entries()) { // Skip expired items if (item.expires && Date.now() > item.expires) { this.delete(key); continue; } if (regex.test(key)) { validKeys.push(key); } } return validKeys; } /** * Get all values matching a pattern * @param {string} pattern - Pattern to match (supports * wildcard) * @returns {object} - Object with matching keys and values */ getByPattern(pattern) { const keys = this.keys(pattern); const result = {}; for (const key of keys) { const value = this.get(key); if (value !== null) { result[key] = value; } } return result; } /** * Clear all values matching a pattern * @param {string} pattern - Pattern to match (supports * wildcard) * @returns {number} - Number of entries cleared */ clearPattern(pattern) { const keys = this.keys(pattern); let cleared = 0; for (const key of keys) { this.delete(key); cleared++; } return cleared; } /** * Clear all memory */ clear() { // Clear all TTL timers for (const timer of this.ttlTimers.values()) { clearTimeout(timer); } this.ttlTimers.clear(); this.store.clear(); this.save(); } /** * Get memory statistics * @returns {object} - Memory stats */ stats() { let totalSize = 0; let expiredCount = 0; let keysWithTTL = 0; let keysWithoutTTL = 0; const namespaces = new Set(); for (const [key, item] of this.store.entries()) { if (item.expires && Date.now() > item.expires) { expiredCount++; continue; } if (item.expires) { keysWithTTL++; } else { keysWithoutTTL++; } totalSize += JSON.stringify(item).length; const namespace = key.split(":")[0]; if (namespace) namespaces.add(namespace); } return { totalKeys: this.store.size, totalEntries: this.store.size, expiredEntries: expiredCount, activeEntries: this.store.size - expiredCount, keysWithTTL, keysWithoutTTL, totalSize, namespaces: Array.from(namespaces), memoryFile: this.memoryFile, }; } /** * Clean up expired entries */ cleanup() { let cleaned = 0; for (const [key, item] of this.store.entries()) { if (item.expires && Date.now() > item.expires) { this.delete(key); cleaned++; } } if (cleaned > 0) { this.save(); } return cleaned; } /** * Save memory to disk */ save() { try { // Ensure directory exists before saving if (!existsSync(this.memoryDir)) { mkdirSync(this.memoryDir, { recursive: true }); } const data = { version: "1.0.0", saved: new Date().toISOString(), entries: Object.fromEntries(this.store), }; writeFileSync(this.memoryFile, JSON.stringify(data, null, 2)); } catch (error) { console.error("Failed to save memory:", error); } } /** * Save memory to disk synchronously (alias for save) */ saveSync() { this.save(); } /** * Load memory from disk */ load() { try { if (!existsSync(this.memoryFile)) { return; } const data = JSON.parse(readFileSync(this.memoryFile, "utf-8")); // Restore entries for (const [key, item] of Object.entries(data.entries || {})) { // Skip expired entries if (item.expires && Date.now() > item.expires) { continue; } this.store.set(key, item); // Restore TTL timer if needed if (item.expires) { const remaining = item.expires - Date.now(); if (remaining > 0) { const timer = setTimeout(() => { this.delete(key); }, remaining); this.ttlTimers.set(key, timer); } } } } catch (error) { console.error("Failed to load memory:", error); } } /** * Context-forge specific methods */ /** * Check if this is a context-forge project * @returns {boolean} */ isContextForgeProject() { return this.get("context-forge:detected") === true; } /** * Get context-forge project configuration * @returns {object|null} */ getContextForgeConfig() { return this.get("context-forge:config"); } /** * Get available PRPs * @returns {array} */ getAvailablePRPs() { return this.get("context-forge:prps") || []; } /** * Get PRP state * @param {string} prpFilename - PRP filename * @returns {object|null} */ getPRPState(prpFilename) { return this.get(`context-forge:prp:${prpFilename}:state`); } /** * Update PRP state * @param {string} prpFilename - PRP filename * @param {object} updates - State updates */ updatePRPState(prpFilename, updates) { const currentState = this.getPRPState(prpFilename) || {}; const newState = { ...currentState, ...updates, lastUpdated: Date.now(), }; this.set(`context-forge:prp:${prpFilename}:state`, newState, 3600000); } /** * Get implementation progress * @returns {object|null} */ getImplementationProgress() { return this.get("context-forge:progress"); } /** * Get stage progress * @param {number} stageNumber - Stage number * @returns {object|null} */ getStageProgress(stageNumber) { return this.get(`context-forge:stage:${stageNumber}`); } /** * Update stage progress * @param {number} stageNumber - Stage number * @param {number} completedTasks - Number of completed tasks */ updateStageProgress(stageNumber, completedTasks) { const stage = this.getStageProgress(stageNumber); if (stage) { stage.completedTasks = completedTasks; stage.progress = (completedTasks / stage.totalTasks) * 100; this.set(`context-forge:stage:${stageNumber}`, stage, 3600000); } } /** * Get available context-forge commands * @returns {array} */ getContextForgeCommands() { return this.get("context-forge:commands") || []; } /** * Track agent action in context-forge project * @param {string} agentName - Name of the agent * @param {string} action - Action performed * @param {object} details - Action details */ trackAgentAction(agentName, action, details = {}) { if (!this.isContextForgeProject()) { return; } const key = `context-forge:agent-actions:${Date.now()}`; this.set( key, { agent: agentName, action, details, timestamp: Date.now(), }, 86400000, ); // 24 hour TTL } /** * Get recent agent actions in context-forge project * @param {number} limit - Number of actions to retrieve * @returns {array} */ getRecentAgentActions(limit = 10) { const pattern = "context-forge:agent-actions:*"; const actions = this.getByPattern(pattern); // Convert to array and sort by timestamp const sortedActions = Object.entries(actions) .map(([key, value]) => value) .sort((a, b) => b.timestamp - a.timestamp) .slice(0, limit); return sortedActions; } /** * Research-specific memory methods for enhanced coordination */ getResearchContext(projectId) { return this.getByPattern(`research:${projectId}:*`); } setResearchPhase(projectId, phase, data) { return this.set(`research:${projectId}:phase:${phase}`, data); } getResearchPhase(projectId, phase) { return this.get(`research:${projectId}:phase:${phase}`); } shareAgentInsights(agentName, insights) { const timestamp = Date.now(); return this.set(`research:shared:insights:${agentName}:${timestamp}`, { agent: agentName, insights, timestamp: new Date().toISOString(), }); } getSharedInsights() { return this.getByPattern("research:shared:insights:*"); } setResearchDeliverable(projectId, deliverableType, content) { return this.set(`research:${projectId}:deliverable:${deliverableType}`, { type: deliverableType, content, generatedAt: new Date().toISOString(), }); } getResearchDeliverables(projectId) { return this.getByPattern(`research:${projectId}:deliverable:*`); } archiveCompletedResearch(projectId) { const researchData = this.getResearchContext(projectId); const archiveKey = `research:archive:${projectId}:${Date.now()}`; this.set(archiveKey, { projectId, archivedAt: new Date().toISOString(), data: researchData, }); // Clean up active research memory Object.keys(researchData).forEach((key) => { if (key.startsWith(`research:${projectId}:`)) { this.delete(key); } }); return archiveKey; } /** * Destroy the memory store */ destroy() { // Clear auto-save interval if (this.autoSaveInterval) { clearInterval(this.autoSaveInterval); } // Clear all TTL timers for (const timer of this.ttlTimers.values()) { clearTimeout(timer); } this.ttlTimers.clear(); this.store.clear(); } } // Export singleton instance let memoryStore = null; export function getMemoryStore(options) { if (!memoryStore) { memoryStore = new SimpleMemoryStore(options); } return memoryStore; } export default SimpleMemoryStore;