UNPKG

@endlessblink/like-i-said-v2

Version:

Task Management & Memory for Claude - Track tasks, remember context, and maintain continuity across sessions with 27 powerful tools. Works with Claude Desktop and Claude Code.

1,390 lines (1,272 loc) 152 kB
#!/usr/bin/env node // Detect if we're running in MCP mode by checking for MCP_MODE env or non-TTY stdin // Windows doesn't always properly detect TTY, so we also check for MCP_MODE env const isMCPMode = process.env.MCP_MODE === 'true' || !process.stdin.isTTY || process.env.MCP_QUIET === 'true'; import fs from 'fs'; import path from 'path'; import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import { DropoffGenerator } from './lib/dropoff-generator.js'; import { TaskStorage } from './lib/task-storage.js'; import { TaskMemoryLinker } from './lib/task-memory-linker.js'; // import { VectorStorage } from './lib/vector-storage.js'; // Removed to prevent @xenova dependency import { TitleSummaryGenerator } from './lib/title-summary-generator.js'; import { OllamaClient } from './lib/ollama-client.js'; import { MemoryDeduplicator } from './lib/memory-deduplicator.js'; import { TaskNLPProcessor } from './lib/task-nlp-processor.js'; import { TaskAutomation } from './lib/task-automation.js'; import { ConversationMonitor } from './lib/conversation-monitor.js'; import { TaskStatusValidator } from './lib/task-status-validator.js'; import { TaskAnalytics } from './lib/task-analytics.js'; import { MemoryTaskAutomator } from './lib/memory-task-automator.cjs'; import { QueryIntelligence } from './lib/query-intelligence.js'; import { BehavioralAnalyzer } from './lib/behavioral-analyzer.js'; import { MemoryEnrichment } from './lib/memory-enrichment.js'; import { SessionTracker } from './lib/session-tracker.js'; import { QueryAnalyzer, RelevanceScorer, ContentClassifier, CircuitBreaker } from './lib/claude-historian-features.js'; import { FuzzyMatcher } from './lib/fuzzy-matching.js'; import { WorkDetectorWrapper } from './lib/work-detector-wrapper.js'; // Removed ConnectionProtection and DataIntegrity imports to prevent any exit calls // import { createRequire } from 'module'; // const require = createRequire(import.meta.url); // const { ConnectionProtection } = require('./lib/connection-protection.cjs'); // const { DataIntegrity } = require('./lib/data-integrity.cjs'); /** * Auto-detect optimal output path for session dropoffs * Option 2: Project-aware detection with Option 5: Parameter override */ function getDropoffOutputPath(customPath = null) { // Option 5: If custom path is provided, use it if (customPath) { const resolvedPath = path.resolve(process.cwd(), customPath); // Ensure the directory exists if (!fs.existsSync(resolvedPath)) { fs.mkdirSync(resolvedPath, { recursive: true }); } return resolvedPath; } // Option 2: Auto-detect session-dropoffs folder const sessionDropoffsPath = path.join(process.cwd(), 'session-dropoffs'); if (fs.existsSync(sessionDropoffsPath)) { return sessionDropoffsPath; } // Fallback: Use current directory return process.cwd(); } // Markdown storage implementation class MarkdownStorage { constructor(baseDir = 'memories', defaultProject = 'default') { this.baseDir = baseDir; this.defaultProject = defaultProject; this.ensureDirectories(); } ensureDirectories() { try { if (!fs.existsSync(this.baseDir)) { fs.mkdirSync(this.baseDir, { recursive: true }); } const defaultProjectDir = path.join(this.baseDir, this.defaultProject); if (!fs.existsSync(defaultProjectDir)) { fs.mkdirSync(defaultProjectDir, { recursive: true }); } } catch (error) { // In NPX mode, directories might need different handling console.error(`Directory creation issue: ${error.message}`); // Continue anyway - the directories might exist but not be detectable } } generateFilename(memory) { const date = new Date(memory.timestamp || Date.now()); const dateStr = date.toISOString().split('T')[0]; const content = memory.content || 'memory'; const slug = content .toLowerCase() .replace(/[^\w\s-]/g, '') .replace(/\s+/g, '-') .slice(0, 30) .replace(/-+$/, ''); const timestamp = Date.now().toString().slice(-6); return `${dateStr}-${slug}-${timestamp}.md`; } getProjectDir(project) { const projectName = project || this.defaultProject; // Security: Sanitize project name to prevent path traversal const sanitizedProject = projectName .replace(/[^a-zA-Z0-9_-]/g, '') .slice(0, 50); if (!sanitizedProject) { throw new Error('Invalid project name'); } const projectDir = path.join(this.baseDir, sanitizedProject); // Security: Ensure the path doesn't escape the base directory const resolvedProjectDir = path.resolve(projectDir); const resolvedBaseDir = path.resolve(this.baseDir); // Detect if we're using an absolute path (NPX mode or custom path) const isAbsolutePath = path.isAbsolute(this.baseDir); // Only enforce strict path traversal checks for relative paths // For absolute paths (NPX mode), we trust the configured directory if (!isAbsolutePath) { // On Windows, normalize paths for comparison const normalizedProjectDir = process.platform === 'win32' ? resolvedProjectDir.toLowerCase().replace(/\\/g, '/') : resolvedProjectDir; const normalizedBaseDir = process.platform === 'win32' ? resolvedBaseDir.toLowerCase().replace(/\\/g, '/') : resolvedBaseDir; if (!normalizedProjectDir.startsWith(normalizedBaseDir)) { if (process.env.DEBUG_MCP) { console.error(`[DEBUG] Path traversal check failed:`); console.error(`[DEBUG] Project dir: ${normalizedProjectDir}`); console.error(`[DEBUG] Base dir: ${normalizedBaseDir}`); } throw new Error('Invalid project path - path traversal attempt detected'); } } if (!fs.existsSync(projectDir)) { fs.mkdirSync(projectDir, { recursive: true }); } return projectDir; } generateMarkdownContent(memory) { // Detect complexity level based on content and metadata const complexity = this.detectComplexityLevel(memory); const frontmatter = [ '---', `id: ${memory.id}`, `timestamp: ${memory.timestamp}`, `complexity: ${complexity}`, memory.category ? `category: ${memory.category}` : null, memory.project ? `project: ${memory.project}` : null, memory.tags && memory.tags.length > 0 ? `tags: [${memory.tags.map(t => `"${t}"`).join(', ')}]` : null, memory.priority ? `priority: ${memory.priority}` : 'priority: medium', memory.status ? `status: ${memory.status}` : 'status: active', memory.related_memories && memory.related_memories.length > 0 ? `related_memories: [${memory.related_memories.map(id => `"${id}"`).join(', ')}]` : null, `access_count: ${memory.access_count || 0}`, `last_accessed: ${memory.last_accessed || memory.timestamp}`, 'metadata:', ` content_type: ${this.detectContentType(memory.content)}`, memory.language ? ` language: ${memory.language}` : null, ` size: ${memory.content.length}`, ` mermaid_diagram: ${this.hasMermaidDiagram(memory.content)}`, '---', '' ].filter(Boolean).join('\n'); return frontmatter + memory.content; } // Detect complexity level (1-4) based on cursor-memory-bank principles detectComplexityLevel(memory) { let complexity = 1; // Default: Simple memory operations // Level 2: Enhanced operations with categorization and tagging if (memory.category || (memory.tags && memory.tags.length > 2)) { complexity = 2; } // Level 3: Project-based organization with cross-references if (memory.project || (memory.related_memories && memory.related_memories.length > 0)) { complexity = 3; } // Level 4: Advanced analytics, relationships, and automation if (memory.content.length > 1000 || (memory.tags && memory.tags.length > 5) || this.hasMermaidDiagram(memory.content) || (memory.related_memories && memory.related_memories.length > 2)) { complexity = 4; } return complexity; } // Detect content type for enhanced metadata detectContentType(content) { // Check for code patterns if (content.includes('```') || content.includes('function') || content.includes('class ') || content.includes('import ') || content.includes('const ') || content.includes('def ') || content.includes('<script') || content.includes('SELECT ') || content.includes('FROM ')) { return 'code'; } // Check for structured data if (content.includes('```json') || content.includes('```yaml') || content.includes('```mermaid') || content.startsWith('{') || content.startsWith('[') || content.includes('---\n')) { return 'structured'; } return 'text'; } // Check if content contains Mermaid diagrams hasMermaidDiagram(content) { return content.includes('```mermaid') || content.includes('graph ') || content.includes('flowchart ') || content.includes('sequenceDiagram') || content.includes('classDiagram') || content.includes('erDiagram'); } parseMarkdownContent(content) { const frontmatterRegex = /^---\n([\s\S]*?)\n---([\s\S]*)$/; const match = content.match(frontmatterRegex); if (!match) { return { id: Date.now().toString(), content: content.trim(), timestamp: new Date().toISOString(), tags: [], complexity: 1, priority: 'medium', status: 'active', access_count: 0 }; } const [, frontmatter, bodyContent] = match; const memory = { content: bodyContent.trim(), metadata: {} }; const lines = frontmatter.split('\n'); let inMetadata = false; lines.forEach(line => { // Handle metadata section if (line.trim() === 'metadata:') { inMetadata = true; return; } if (inMetadata && line.startsWith(' ')) { // Parse metadata fields const metaLine = line.slice(2); // Remove 2-space indent const colonIndex = metaLine.indexOf(':'); if (colonIndex === -1) return; const key = metaLine.slice(0, colonIndex).trim(); const value = metaLine.slice(colonIndex + 1).trim(); switch (key) { case 'content_type': memory.metadata.content_type = value; break; case 'language': memory.metadata.language = value; break; case 'size': memory.metadata.size = parseInt(value) || 0; break; case 'mermaid_diagram': memory.metadata.mermaid_diagram = value === 'true'; break; } return; } inMetadata = false; // Parse main frontmatter fields const colonIndex = line.indexOf(':'); if (colonIndex === -1) return; const key = line.slice(0, colonIndex).trim(); const value = line.slice(colonIndex + 1).trim(); switch (key) { case 'id': memory.id = value; break; case 'timestamp': case 'created': memory.timestamp = value; break; case 'complexity': memory.complexity = parseInt(value) || 1; break; case 'category': memory.category = value; break; case 'priority': memory.priority = value; break; case 'status': memory.status = value; break; case 'access_count': memory.access_count = parseInt(value) || 0; break; case 'last_accessed': memory.last_accessed = value; break; case 'tags': if (value.startsWith('[') && value.endsWith(']')) { memory.tags = value.slice(1, -1).split(',').map(t => t.trim().replace(/['"]/g, '')); } else { memory.tags = value.split(',').map(t => t.trim()).filter(Boolean); } break; case 'related_memories': if (value.startsWith('[') && value.endsWith(']')) { memory.related_memories = value.slice(1, -1).split(',').map(t => t.trim().replace(/['"]/g, '')); } else { memory.related_memories = value.split(',').map(t => t.trim()).filter(Boolean); } break; case 'project': memory.project = value; break; } }); return memory; } parseMarkdownFile(filepath) { try { const content = fs.readFileSync(filepath, 'utf8'); const parsed = this.parseMarkdownContent(content); if (!parsed) return null; const filename = path.basename(filepath); const projectName = path.basename(path.dirname(filepath)); return { ...parsed, filename, filepath, project: projectName === this.defaultProject ? undefined : projectName }; } catch (error) { console.error(`Error reading markdown file ${filepath}:`, error); return null; } } async saveMemory(memory) { try { // Check if this memory already exists const existingMemory = await this.getMemory(memory.id); let filepath; if (existingMemory && existingMemory.filepath) { // Update existing memory file at its current location filepath = existingMemory.filepath; } else { // Create new memory file const projectDir = this.getProjectDir(memory.project); const filename = this.generateFilename(memory); filepath = path.join(projectDir, filename); } const markdownContent = this.generateMarkdownContent(memory); // Ensure directory exists before writing const dir = path.dirname(filepath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } fs.writeFileSync(filepath, markdownContent, 'utf8'); return filepath; } catch (error) { if (process.env.DEBUG_MCP) console.error(`[ERROR] saveMemory failed:`, error); throw error; } } async getMemory(id) { const memories = await this.listMemories(); return memories.find(m => m.id === id) || null; } async listMemories(project) { const memories = []; // Safeguard: Only load from the designated memories directory if (!fs.existsSync(this.baseDir)) { console.warn(`Memories directory '${this.baseDir}' does not exist. Creating it...`); fs.mkdirSync(this.baseDir, { recursive: true }); return memories; } // Safeguard: Basic validation only - ensure it's a valid directory path const resolvedBaseDir = path.resolve(this.baseDir); // Remove the restrictive check that breaks NPX installations // The directory is already validated during initialization if (project) { const projectDir = this.getProjectDir(project); // Project directory validation removed - trust the configured paths if (!fs.existsSync(projectDir)) { return memories; } const files = fs.readdirSync(projectDir).filter(f => f.endsWith('.md')); for (const file of files) { const filepath = path.join(projectDir, file); const memory = this.parseMarkdownFile(filepath); if (memory) memories.push(memory); } } else { const projectDirs = fs.readdirSync(this.baseDir).filter(dir => { const dirPath = path.join(this.baseDir, dir); return fs.statSync(dirPath).isDirectory(); }); for (const projectDir of projectDirs) { const projectPath = path.join(this.baseDir, projectDir); // Trust the project paths - no validation needed const files = fs.readdirSync(projectPath).filter(f => f.endsWith('.md')); for (const file of files) { const filepath = path.join(projectPath, file); // Trust the file paths - no validation needed const memory = this.parseMarkdownFile(filepath); if (memory) memories.push(memory); } } } return memories.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()); } async updateMemory(id, updates) { const existingMemory = await this.getMemory(id); if (!existingMemory) return false; const updatedMemory = { ...existingMemory, ...updates, id: existingMemory.id, timestamp: existingMemory.timestamp }; await this.saveMemory(updatedMemory); return true; } async deleteMemory(id) { const memory = await this.getMemory(id); if (!memory) return false; fs.unlinkSync(memory.filepath); return true; } async searchMemories(query, project) { const memories = await this.listMemories(project); const lowerQuery = query.toLowerCase(); return memories.filter(memory => memory.content.toLowerCase().includes(lowerQuery) || memory.tags?.some(tag => tag.toLowerCase().includes(lowerQuery)) || memory.category?.toLowerCase().includes(lowerQuery) ); } // Migration from JSON async migrateFromJSON(jsonFilePath) { if (!fs.existsSync(jsonFilePath)) { if (process.env.DEBUG_MCP) console.error('No JSON file to migrate from'); return 0; } const jsonContent = fs.readFileSync(jsonFilePath, 'utf8'); const memories = JSON.parse(jsonContent); let migrated = 0; for (const memory of memories) { try { await this.saveMemory(memory); migrated++; } catch (error) { console.error(`Failed to migrate memory ${memory.id}:`, error); } } const backupPath = jsonFilePath + '.backup'; fs.copyFileSync(jsonFilePath, backupPath); if (!isMCPMode && !process.env.MCP_QUIET) console.error(`Migrated ${migrated} memories. JSON backup saved to ${backupPath}`); return migrated; } } // Load saved configuration if it exists let savedConfig = {}; const configPath = path.join(process.cwd(), '.like-i-said-config.json'); if (fs.existsSync(configPath)) { try { savedConfig = JSON.parse(fs.readFileSync(configPath, 'utf8')); } catch (e) { // Ignore parse errors } } // Get memory and task directories from saved config, environment variables, or use defaults let MEMORY_DIR = savedConfig.memoryDir || process.env.MEMORY_DIR || process.env.LIKE_I_SAID_MEMORY_DIR || 'memories'; let TASK_DIR = savedConfig.taskDir || process.env.TASK_DIR || process.env.LIKE_I_SAID_TASK_DIR || 'tasks'; // Log the directories being used (only in non-MCP mode) if (!isMCPMode) { console.log(`Memory directory: ${path.resolve(MEMORY_DIR)}`); console.log(`Task directory: ${path.resolve(TASK_DIR)}`); } // Initialize storage with custom directory let storage = new MarkdownStorage(MEMORY_DIR); // Initialize vector storage - stub to remove @xenova dependency const vectorStorage = { initialized: false, initialize: async () => {}, addMemory: async () => {}, addTask: async () => {}, searchSimilar: async () => [], rebuildIndex: async () => {} }; // Initialize conversation monitor for automatic memory detection const conversationMonitor = new ConversationMonitor(storage, vectorStorage); // Listen for automatic memory creation events conversationMonitor.on('memory-created', (event) => { console.error(`🤖 Auto-captured memory: ${event.memory.id} - ${event.reason}`); }); // Initialize advanced memory systems const queryIntelligence = new QueryIntelligence(); const behavioralAnalyzer = new BehavioralAnalyzer(); const memoryEnrichment = new MemoryEnrichment(storage, vectorStorage); const sessionTracker = new SessionTracker(storage); // Initialize Universal Work Detector (disabled by default for safety) const workDetector = new WorkDetectorWrapper({ enabled: false, // Start disabled for safety debugMode: true, safeMode: true }); // Initialize claude-historian inspired features const queryAnalyzer = new QueryAnalyzer(); const relevanceScorer = new RelevanceScorer(); const contentClassifier = new ContentClassifier(); const circuitBreaker = new CircuitBreaker(); const fuzzyMatcher = new FuzzyMatcher(); // Initialize task storage and linker with custom directory let taskStorage = new TaskStorage(TASK_DIR, storage); let taskMemoryLinker = new TaskMemoryLinker(storage, taskStorage); // Initialize memory-task automator const memoryTaskAutomator = new MemoryTaskAutomator(storage, taskStorage, { enabled: true, minConfidence: 0.5, autoExecuteThreshold: 0.8, logAutomatedActions: true }); // Initialize dropoff generator const dropoffGenerator = new DropoffGenerator(); // Initialize connection protection system (disabled in MCP mode to prevent disconnections) // Disable modules that cause exits - ALWAYS disabled for MCP stability let connectionProtection = null; let dataIntegrity = null; // These modules are disabled to prevent process.exit() calls that break MCP connection // Auto-migrate from JSON if it exists (only once) const jsonFile = path.join(process.cwd(), 'memories.json'); const migrationMarker = path.join(process.cwd(), '.migration-complete'); if (fs.existsSync(jsonFile) && !fs.existsSync(migrationMarker)) { // Suppress ALL migration messages in MCP mode if (!isMCPMode) console.error('Migrating from JSON to markdown files...'); storage.migrateFromJSON(jsonFile).then(count => { if (!isMCPMode) console.error(`Migration complete: ${count} memories converted to markdown`); // Create marker to prevent re-migration fs.writeFileSync(migrationMarker, new Date().toISOString()); }); } // NEVER show startup messages in MCP mode // Startup message disabled to prevent MCP protocol corruption // Create MCP server const server = new Server( { name: 'like-i-said-memory-v2', version: '2.0.0', }, { capabilities: { tools: {}, }, } ); // List available tools server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'add_memory', description: 'AUTOMATICALLY use when user shares important information, code snippets, decisions, learnings, or context that should be remembered for future sessions. Includes smart categorization and auto-linking.', inputSchema: { type: 'object', properties: { content: { type: 'string', description: 'The memory content to store', }, tags: { type: 'array', items: { type: 'string' }, description: 'Optional tags for the memory', }, category: { type: 'string', description: 'Memory category (personal, work, code, research, conversations, preferences)', }, project: { type: 'string', description: 'Project name to organize memory files', }, priority: { type: 'string', description: 'Priority level (low, medium, high)', }, status: { type: 'string', description: 'Memory status (active, archived, reference)', }, related_memories: { type: 'array', items: { type: 'string' }, description: 'IDs of related memories for cross-referencing', }, language: { type: 'string', description: 'Programming language for code content', }, }, required: ['content'], }, }, { name: 'get_memory', description: 'Retrieve a memory by ID', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'The memory ID to retrieve', }, }, required: ['id'], }, }, { name: 'list_memories', description: 'List all stored memories or memories from a specific project', inputSchema: { type: 'object', properties: { limit: { type: 'number', description: 'Maximum number of memories to return', }, project: { type: 'string', description: 'Filter by project name', }, }, }, }, { name: 'delete_memory', description: 'Delete a memory by ID', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'The memory ID to delete', }, }, required: ['id'], }, }, { name: 'search_memories', description: 'AUTOMATICALLY use when user asks about past work, previous decisions, looking for examples, or needs context from earlier sessions. Provides semantic and keyword-based search.', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query', }, project: { type: 'string', description: 'Limit search to specific project', }, }, required: ['query'], }, }, { name: 'test_tool', description: 'Simple test tool to verify MCP is working', inputSchema: { type: 'object', properties: { message: { type: 'string', description: 'Test message', }, }, required: ['message'], }, }, { name: 'generate_dropoff', description: 'Generate conversation dropoff document for session handoff with context from recent memories, git status, and project info', inputSchema: { type: 'object', properties: { session_summary: { type: 'string', description: 'Brief summary of work done in this session', default: 'Session work completed', }, include_recent_memories: { type: 'boolean', description: 'Include recent memories in the dropoff', default: true, }, include_git_status: { type: 'boolean', description: 'Include git status and recent commits', default: true, }, recent_memory_count: { type: 'number', description: 'Number of recent memories to include', default: 5, }, output_format: { type: 'string', description: 'Output format: markdown or json', enum: ['markdown', 'json'], default: 'markdown', }, output_path: { type: 'string', description: 'Custom output directory path. If not provided, auto-detects session-dropoffs/ folder or defaults to current directory', default: null, }, }, }, }, { name: 'create_task', description: 'Create a new task with intelligent memory linking. Tasks start in "todo" status. IMPORTANT: After creating a task, remember to update its status to "in_progress" when you begin working on it. Proper state management helps track workflow and productivity.', inputSchema: { type: 'object', properties: { title: { type: 'string', description: 'Task title', }, description: { type: 'string', description: 'Detailed task description', }, project: { type: 'string', description: 'Project identifier', }, category: { type: 'string', enum: ['personal', 'work', 'code', 'research'], description: 'Task category', }, priority: { type: 'string', enum: ['low', 'medium', 'high', 'urgent'], default: 'medium', description: 'Task priority', }, parent_task: { type: 'string', description: 'Parent task ID for subtasks', }, manual_memories: { type: 'array', items: { type: 'string' }, description: 'Memory IDs to manually link', }, tags: { type: 'array', items: { type: 'string' }, description: 'Task tags', }, auto_link: { type: 'boolean', default: true, description: 'Automatically find and link relevant memories', }, }, required: ['title', 'project'], }, }, { name: 'update_task', description: `Update task status and details. STATE MANAGEMENT GUIDELINES: - Always mark tasks as "in_progress" when starting work on them - Update to "done" immediately after completing a task - Set to "blocked" when encountering obstacles or dependencies - Use "todo" for tasks not yet started IMPORTANT: Proactively manage task states throughout the work lifecycle. Don't wait for user prompts - update states as work progresses to maintain accurate workflow visibility.`, inputSchema: { type: 'object', properties: { task_id: { type: 'string', description: 'Task ID to update', }, status: { type: 'string', enum: ['todo', 'in_progress', 'done', 'blocked'], description: 'New task status', }, title: { type: 'string', description: 'New task title', }, description: { type: 'string', description: 'New task description', }, add_subtasks: { type: 'array', items: { type: 'string' }, description: 'Task titles to add as subtasks', }, add_memories: { type: 'array', items: { type: 'string' }, description: 'Memory IDs to link', }, remove_memories: { type: 'array', items: { type: 'string' }, description: 'Memory IDs to unlink', }, }, required: ['task_id'], }, }, { name: 'list_tasks', description: 'List tasks with filtering options. Shows task status distribution and workflow health. Use this to monitor work progress and identify tasks that need status updates.', inputSchema: { type: 'object', properties: { project: { type: 'string', description: 'Filter by project', }, status: { type: 'string', enum: ['todo', 'in_progress', 'done', 'blocked'], description: 'Filter by status', }, category: { type: 'string', description: 'Filter by category', }, has_memory: { type: 'string', description: 'Filter by memory connection', }, include_subtasks: { type: 'boolean', default: true, description: 'Include subtasks in results', }, limit: { type: 'number', default: 20, description: 'Maximum tasks to return', }, }, }, }, { name: 'get_task_context', description: 'Get detailed task information including status, relationships, and connected memories. Use this to understand task context and determine if status updates are needed.', inputSchema: { type: 'object', properties: { task_id: { type: 'string', description: 'Task ID', }, depth: { type: 'string', enum: ['direct', 'deep'], default: 'direct', description: 'How many levels of connections to traverse', }, }, required: ['task_id'], }, }, { name: 'delete_task', description: 'Delete a task and its subtasks', inputSchema: { type: 'object', properties: { task_id: { type: 'string', description: 'Task ID to delete', }, }, required: ['task_id'], }, }, { name: 'enhance_memory_metadata', description: 'Generate optimized title and summary for a memory to improve dashboard card display. Uses intelligent content analysis to create concise, meaningful titles (max 60 chars) and summaries (max 150 chars).', inputSchema: { type: 'object', properties: { memory_id: { type: 'string', description: 'The ID of the memory to enhance with title and summary', }, regenerate: { type: 'boolean', description: 'Force regeneration even if title/summary already exist', }, }, required: ['memory_id'], }, }, { name: 'batch_enhance_memories', description: 'Batch process multiple memories to add optimized titles and summaries. Useful for enhancing existing memories that lack proper metadata for dashboard display.', inputSchema: { type: 'object', properties: { project: { type: 'string', description: 'Filter by project name (optional)', }, category: { type: 'string', description: 'Filter by category (optional)', }, limit: { type: 'number', description: 'Maximum number of memories to process (default: 50)', }, skip_existing: { type: 'boolean', description: 'Skip memories that already have titles/summaries (default: true)', }, }, }, }, { name: 'smart_status_update', description: 'AUTOMATICALLY use when user mentions status changes in natural language. Intelligently parses natural language to determine intended status changes with validation and automation.', inputSchema: { type: 'object', properties: { task_id: { type: 'string', description: 'Task ID to update (optional - can be inferred from natural language)', }, natural_language_input: { type: 'string', description: 'Natural language description of the status change (e.g., "I finished the auth module", "the API work is blocked")', }, context: { type: 'object', properties: { force_complete: { type: 'boolean' }, skip_validation: { type: 'boolean' }, blocking_reason: { type: 'string' }, completion_evidence: { type: 'string' } }, description: 'Additional context for intelligent processing', }, apply_automation: { type: 'boolean', description: 'Whether to apply automation suggestions (default: true)', }, }, required: ['natural_language_input'], }, }, { name: 'get_task_status_analytics', description: 'AUTOMATICALLY use when user asks about task progress, status overview, productivity metrics, or wants analytics. Provides comprehensive status insights and recommendations.', inputSchema: { type: 'object', properties: { project: { type: 'string', description: 'Filter analytics by project (optional)', }, time_range: { type: 'string', enum: ['day', 'week', 'month', 'quarter'], description: 'Time range for analytics (default: week)', }, include_trends: { type: 'boolean', description: 'Include trend analysis (default: true)', }, include_recommendations: { type: 'boolean', description: 'Include actionable recommendations (default: true)', }, include_project_breakdown: { type: 'boolean', description: 'Include project-by-project analysis (default: true)', }, }, }, }, { name: 'validate_task_workflow', description: 'Validate a proposed task status change with intelligent suggestions and workflow analysis. Use when you need to check if a status change makes sense.', inputSchema: { type: 'object', properties: { task_id: { type: 'string', description: 'Task ID to validate', }, proposed_status: { type: 'string', enum: ['todo', 'in_progress', 'done', 'blocked'], description: 'Proposed new status', }, context: { type: 'object', properties: { force_complete: { type: 'boolean' }, skip_testing: { type: 'boolean' }, skip_review: { type: 'boolean' }, blocking_reason: { type: 'string' } }, description: 'Additional context for validation', }, }, required: ['task_id', 'proposed_status'], }, }, { name: 'get_automation_suggestions', description: 'Get intelligent automation suggestions for a task based on context analysis. Use when you want to see what automated actions are possible.', inputSchema: { type: 'object', properties: { task_id: { type: 'string', description: 'Task ID to analyze for automation opportunities', }, }, required: ['task_id'], }, }, { name: 'batch_enhance_memories_ollama', description: 'Batch process memories using local AI (Ollama) for privacy-focused title/summary generation. Processes large numbers of memories efficiently without external API calls.', inputSchema: { type: 'object', properties: { project: { type: 'string', description: 'Filter by project name (optional)', }, category: { type: 'string', description: 'Filter by category (optional)', }, limit: { type: 'number', description: 'Maximum number of memories to process (default: 50)', }, skip_existing: { type: 'boolean', description: 'Skip memories that already have titles/summaries (default: true)', }, model: { type: 'string', description: 'Ollama model to use (default: llama3.1:8b)', }, batch_size: { type: 'number', description: 'Number of memories to process in parallel (default: 5)', }, }, }, }, { name: 'batch_enhance_tasks_ollama', description: 'Batch process tasks using local AI (Ollama) for privacy-focused title/description enhancement. Processes large numbers of tasks efficiently without external API calls.', inputSchema: { type: 'object', properties: { project: { type: 'string', description: 'Filter by project name (optional)', }, category: { type: 'string', description: 'Filter by category (optional)', }, status: { type: 'string', description: 'Filter by task status (optional)', }, limit: { type: 'number', description: 'Maximum number of tasks to process (default: 50)', }, skip_existing: { type: 'boolean', description: 'Skip tasks that already have enhanced titles/descriptions (default: true)', }, model: { type: 'string', description: 'Ollama model to use (default: llama3.1:8b)', }, batch_size: { type: 'number', description: 'Number of tasks to process in parallel (default: 5)', }, }, }, }, { name: 'check_ollama_status', description: 'Check if Ollama server is running and list available models for local AI processing.', inputSchema: { type: 'object', properties: { show_models: { type: 'boolean', description: 'Whether to list available models (default: true)', }, }, }, }, { name: 'enhance_memory_ollama', description: 'Enhance a single memory with local AI (Ollama) for privacy-focused title/summary generation.', inputSchema: { type: 'object', properties: { memory_id: { type: 'string', description: 'ID of the memory to enhance', }, model: { type: 'string', description: 'Ollama model to use (default: llama3.1:8b)', }, force_update: { type: 'boolean', description: 'Force update even if memory already has title/summary (default: false)', }, }, required: ['memory_id'], }, }, { name: 'deduplicate_memories', description: 'Find and remove duplicate memory files, keeping the newest version of each memory ID. Use this to clean up duplicate memories caused by batch operations.', inputSchema: { type: 'object', properties: { preview_only: { type: 'boolean', description: 'Preview what would be removed without actually deleting files (default: false)', }, }, }, }, { name: 'work_detector_control', description: 'Control the Universal Work Detector for automatic memory creation based on work patterns.', inputSchema: { type: 'object', properties: { action: { type: 'string', enum: ['enable', 'disable', 'status', 'stats'], description: 'Action to perform: enable, disable, status, or stats', }, }, required: ['action'], }, }, { name: 'set_memory_path', description: 'Change where memories are stored. Updates the path dynamically without requiring restart.', inputSchema: { type: 'object', properties: { path: { type: 'string', description: 'New absolute path for memory storage (e.g., D:\\MyDocuments\\AI-Memories)', }, }, required: ['path'], }, }, { name: 'set_task_path', description: 'Change where tasks are stored. Updates the path dynamically without requiring restart.', inputSchema: { type: 'object', properties: { path: { type: 'string', description: 'New absolute path for task storage (e.g., D:\\MyDocuments\\AI-Tasks)', }, }, required: ['path'], }, }, { name: 'get_current_paths', description: 'Get the current memory and task storage paths.', inputSchema: { type: 'object', properties: {}, }, }, ], }; }); // Enhanced tool selection logic with auto-trigger detection function shouldAutoTrigger(toolName, userContext = '') { const autoTriggerKeywords = { 'create_task': ['create', 'add', 'start', 'implement', 'build', 'make', 'work on', 'new task', 'new feature', 'new bug', 'todo', 'need to'], 'update_task': ['update', 'modify', 'change', 'complete', 'finish', 'done', 'block', 'progress', 'status', 'mark as'], 'smart_status_update': ['finished', 'completed', 'done with', 'blocked on', 'stuck on', 'working on', 'started', 'began', 'wrapped up', 'closed'], 'add_memory': ['remember', 'save', 'store', 'important', 'note', 'learned', 'decision', 'context', 'keep track'], 'search_memories': ['what did', 'how did', 'previous', 'before', 'earlier', 'last time', 'remember when', 'find', 'look for'], 'list_tasks': ['what am I', 'current tasks', 'working on', 'todo list', 'project status', 'progress', 'overview'], 'get_task_context': ['details about', 'more info', 'task info', 'related to', 'context for', 'tell me about', 'tell me more', 'more about'], 'get_task_status_analytics': ['analytics', 'metrics', 'progress report', 'how am I doing', 'productivity', 'status overview', 'completion rate', 'trends'], 'validate_task_workflow': ['is it okay to', 'can I', 'should I', 'validate', 'check if', 'makes sense to'], 'get_automation_suggestions': ['automation', 'suggestions', 'what can be automated', 'smart suggestions', 'recommendations'] }; const keywords = autoTriggerKeywords[toolName] || []; const lowerContext = userContext.toLowerCase(); return keywords.some(keyword => lowerContext.includes(keyword)); } // Handle tool calls server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; // Track tool usage for behavioral analysis const toolStartTime = Date.now(); let toolResult = null; let toolError = null; try { // Track activity sessionTracker.trackActivity('tool_use', { tool: name, args }); // Universal Work Detector tracking (safe mode enabled) const workDetection = workDetector.trackActivity(name, args, null); switch (name) { case 'add_memory': { const { content, tags = [], category, project, priority = 'medium', status = 'active', related_memories = [], language } = args; // Safeguard: Validate against mock data patterns if (!content || typeof content !== 'string' || content.trim().length === 0) { throw new Error('Invalid memory: Content is required and must be a non-empty string'); } // Safeguard: Reject mock data indicators const mockDataPatterns = [ /mock-\d+/i, /test.*data/i, /sample.*content/i, /lorem ipsum/i, /fake.*data/i, /placeholder/i ]; const containsMockPattern = mockDataPatterns.some(pattern => pattern.test(content) || (typeof project === 'string' && pattern.test(project)) || (Array.isArray(tags) && tags.some(tag => pattern.test(tag))) ); if (containsMockPattern) { throw new Error('Invalid memory: Mock data patterns detected. Only real memories are allowed.'); } // Safeguard: Validate real content requirements if (content.trim().length < 10) { throw new Erro