UNPKG

@tosin2013/kanbn

Version:

A CLI Kanban board with AI-powered task management features

325 lines (282 loc) 9.44 kB
/** * Memory Manager for Kanbn * Handles loading and saving chat history and context */ const fs = require('fs'); const path = require('path'); const util = require('util'); const readFile = util.promisify(fs.readFile); const writeFile = util.promisify(fs.writeFile); const mkdir = util.promisify(fs.mkdir); class MemoryManager { /** * Create a new MemoryManager * @param {string} kanbnFolder The path to the .kanbn folder */ constructor(kanbnFolder) { this.kanbnFolder = kanbnFolder; // Ensure memory file is stored in the .kanbn directory // Check if the path already contains .kanbn, otherwise append it const kanbnDir = kanbnFolder.endsWith('.kanbn') ? kanbnFolder : path.join(kanbnFolder, '.kanbn'); this.memoryFile = path.join(kanbnDir, 'chat-memory.json'); this.memory = { conversations: [], context: {}, taskReferences: {}, // Track task reference history lastUpdated: null }; } /** * Load memory from disk * @returns {Promise<Object>} The loaded memory */ async loadMemory() { try { if (fs.existsSync(this.memoryFile)) { const data = await readFile(this.memoryFile, 'utf8'); this.memory = JSON.parse(data); } else { // Initialize with empty memory this.memory = { conversations: [], context: {}, taskReferences: {}, // Add empty task references structure lastUpdated: new Date().toISOString() }; await this.saveMemory(); } return this.memory; } catch (error) { console.error('Error loading memory:', error); // Return default memory if there's an error return { conversations: [], context: {}, taskReferences: {}, // Include task references in default return value lastUpdated: new Date().toISOString() }; } } /** * Save memory to disk * @returns {Promise<void>} */ async saveMemory() { try { // Get directory path from memory file path const dirPath = path.dirname(this.memoryFile); console.log(`Saving memory to file: ${this.memoryFile}`); console.log(`Directory path: ${dirPath}`); // Ensure the directory exists try { console.log(`Creating directory: ${dirPath}`); await mkdir(dirPath, { recursive: true }); console.log('Directory created or already exists'); } catch (mkdirError) { // Ignore if directory already exists if (mkdirError.code !== 'EEXIST') { console.error(`Error creating directory: ${mkdirError.message}`); throw mkdirError; } console.log('Directory already exists'); } // Update the lastUpdated timestamp this.memory.lastUpdated = new Date().toISOString(); // Debug memory contents console.log(`Memory contents: Task references: ${Object.keys(this.memory.taskReferences).length}, Conversations: ${this.memory.conversations.length}`); // Write the memory to disk await writeFile( this.memoryFile, JSON.stringify(this.memory, null, 2), 'utf8' ); console.log(`Memory successfully saved to ${this.memoryFile}`); } catch (error) { console.error(`Error saving memory to ${this.memoryFile}:`, error); } } /** * Add a message to the conversation history * @param {string} role The role of the message sender ('user' or 'assistant') * @param {string} content The message content * @param {string} type The type of conversation ('chat' or 'init') * @returns {Promise<void>} */ async addMessage(role, content, type = 'chat') { // Find the most recent conversation of the specified type let conversation = this.memory.conversations.find(c => c.type === type && c.active); // If no active conversation of this type exists, create one if (!conversation) { conversation = { id: Date.now().toString(), type, active: true, messages: [], created: new Date().toISOString(), updated: new Date().toISOString() }; this.memory.conversations.push(conversation); } // Add the message to the conversation conversation.messages.push({ role, content, timestamp: new Date().toISOString() }); // Update the conversation's updated timestamp conversation.updated = new Date().toISOString(); // Save the updated memory await this.saveMemory(); } /** * Get the conversation history for a specific type * @param {string} type The type of conversation ('chat' or 'init') * @returns {Array} The conversation history */ getConversationHistory(type = 'chat') { // Find the most recent conversation of the specified type const conversation = this.memory.conversations.find(c => c.type === type && c.active); // If no conversation exists, return an empty array if (!conversation) { return []; } // Return the messages in the format expected by OpenRouter return conversation.messages.map(message => ({ role: message.role, content: message.content })); } /** * Update the context with new information * @param {Object} newContext The new context information * @returns {Promise<void>} */ async updateContext(newContext) { // Merge the new context with the existing context this.memory.context = { ...this.memory.context, ...newContext }; // Save the updated memory await this.saveMemory(); } /** * Get the current context * @returns {Object} The current context */ getContext() { return this.memory.context; } /** * Start a new conversation * @param {string} type The type of conversation ('chat' or 'init') * @returns {Promise<void>} */ async startNewConversation(type = 'chat') { // Mark all existing conversations of this type as inactive this.memory.conversations.forEach(conversation => { if (conversation.type === type) { conversation.active = false; } }); // Create a new conversation const newConversation = { id: Date.now().toString(), type, active: true, messages: [], created: new Date().toISOString(), updated: new Date().toISOString() }; // Add the new conversation to the memory this.memory.conversations.push(newConversation); // Save the updated memory await this.saveMemory(); } /** * Clear all memory * @returns {Promise<void>} */ async clearMemory() { this.memory = { conversations: [], context: {}, taskReferences: {}, // Include task references when clearing memory lastUpdated: new Date().toISOString() }; await this.saveMemory(); } /** * Record a task reference in the memory * @param {string} taskId The ID of the task being referenced * @param {Object} taskData Additional task data to store (name, status, etc.) * @param {string} referenceType The type of reference ('view', 'mention', 'update') * @returns {Promise<void>} */ async addTaskReference(taskId, taskData = {}, referenceType = 'view') { if (!taskId) return; // Initialize task reference if it doesn't exist if (!this.memory.taskReferences[taskId]) { this.memory.taskReferences[taskId] = { references: [], firstReferenced: new Date().toISOString(), lastReferenced: new Date().toISOString(), data: {} }; } // Update the task reference const taskRef = this.memory.taskReferences[taskId]; // Add the new reference taskRef.references.push({ type: referenceType, timestamp: new Date().toISOString(), conversationId: this.getActiveConversationId() }); // Keep only the last 10 references to prevent unlimited growth if (taskRef.references.length > 10) { taskRef.references = taskRef.references.slice(-10); } // Update the last referenced timestamp taskRef.lastReferenced = new Date().toISOString(); // Merge any new task data if (taskData && typeof taskData === 'object') { taskRef.data = { ...taskRef.data, ...taskData }; } // Save the updated memory await this.saveMemory(); } /** * Get task reference history * @param {string} taskId The ID of the task (optional, if not provided returns all task references) * @returns {Object} The task reference history */ getTaskReferences(taskId = null) { if (taskId) { return this.memory.taskReferences[taskId] || null; } return this.memory.taskReferences; } /** * Check if a task has been previously referenced * @param {string} taskId The ID of the task * @returns {boolean} Whether the task has been referenced before */ hasTaskBeenReferenced(taskId) { return !!this.memory.taskReferences[taskId]; } /** * Get the ID of the active conversation * @param {string} type The conversation type * @returns {string|null} The conversation ID or null if no active conversation */ getActiveConversationId(type = 'chat') { const conversation = this.memory.conversations.find(c => c.type === type && c.active); return conversation ? conversation.id : null; } } module.exports = MemoryManager;