@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.
488 lines (410 loc) • 16.3 kB
JavaScript
import { VectorStorage } from './vector-storage.js';
export class TaskMemoryLinker {
constructor(memoryStorage, taskStorage) {
this.memoryStorage = memoryStorage;
this.taskStorage = taskStorage;
this.vectorStorage = new VectorStorage();
this.stopWords = new Set([
'the', 'is', 'at', 'which', 'on', 'a', 'an', 'and', 'or', 'but',
'in', 'with', 'to', 'for', 'of', 'as', 'by', 'that', 'this',
'it', 'from', 'be', 'are', 'been', 'being', 'have', 'has', 'had',
'do', 'does', 'did', 'will', 'would', 'should', 'could', 'may', 'might'
]);
}
async autoLinkMemories(task) {
console.error(`[TaskMemoryLinker] Auto-linking memories for task: ${task.title}`);
// Get candidates from both keyword and semantic search
const keywordCandidates = await this.findCandidateMemories(task);
const semanticCandidates = await this.findSemanticCandidates(task);
// Combine and deduplicate candidates
const allCandidates = this.combineAndDeduplicateCandidates(keywordCandidates, semanticCandidates);
// Rank using hybrid approach
const ranked = await this.rankByRelevance(allCandidates, task);
const selected = ranked.filter(m => m.relevance > 0.3).slice(0, 5);
console.error(`[TaskMemoryLinker] Found ${allCandidates.length} candidates (${keywordCandidates.length} keyword, ${semanticCandidates.length} semantic), selected ${selected.length} with relevance > 0.3`);
return selected.map(memory => ({
memory_id: memory.id,
memory_serial: memory.serial || `MEM-${memory.id.substring(0, 6)}`,
connection_type: this.determineConnectionType(memory, task),
relevance: memory.relevance,
matched_terms: memory.matched_terms || []
}));
}
async findCandidateMemories(task) {
const memories = await this.memoryStorage.listMemories();
const terms = this.extractSearchTerms(task);
console.error(`[TaskMemoryLinker] Extracted terms:`, terms);
// Filter memories by various criteria
const candidates = [];
for (const memory of memories) {
let isCandidate = false;
const matchedTerms = [];
// Project match
if (memory.project === task.project) {
isCandidate = true;
matchedTerms.push(`project:${task.project}`);
}
// Category match
if (memory.category === task.category) {
isCandidate = true;
matchedTerms.push(`category:${task.category}`);
}
// Tag matches
const memoryTags = memory.tags || [];
const taskTags = task.tags || [];
const commonTags = memoryTags.filter(tag => taskTags.includes(tag));
if (commonTags.length > 0) {
isCandidate = true;
matchedTerms.push(...commonTags.map(tag => `tag:${tag}`));
}
// Keyword matches in content
const contentLower = memory.content.toLowerCase();
for (const keyword of terms.keywords) {
if (contentLower.includes(keyword.toLowerCase())) {
isCandidate = true;
matchedTerms.push(keyword);
}
}
// Technical term matches
for (const tech of terms.technical) {
if (memory.content.includes(tech)) {
isCandidate = true;
matchedTerms.push(`tech:${tech}`);
}
}
// Time proximity (last 14 days)
const memoryDate = new Date(memory.timestamp);
const taskDate = new Date(task.created);
const daysDiff = Math.abs(taskDate - memoryDate) / (1000 * 60 * 60 * 24);
if (daysDiff <= 14) {
isCandidate = true;
matchedTerms.push(`time:${Math.round(daysDiff)}d`);
}
if (isCandidate) {
candidates.push({
...memory,
matched_terms: [...new Set(matchedTerms)]
});
}
}
return candidates;
}
async findSemanticCandidates(task) {
try {
const semanticResults = await this.vectorStorage.findRelevantMemories(task, 10);
// Get full memory objects for semantic results
const candidates = [];
for (const result of semanticResults) {
const memory = await this.memoryStorage.getMemory(result.id);
if (memory) {
candidates.push({
...memory,
semantic_score: result.relevance,
matched_terms: ['semantic_match']
});
}
}
return candidates;
} catch (error) {
console.error('[TaskMemoryLinker] Semantic search failed:', error);
return [];
}
}
combineAndDeduplicateCandidates(keywordCandidates, semanticCandidates) {
const candidateMap = new Map();
// Add keyword candidates
for (const candidate of keywordCandidates) {
candidateMap.set(candidate.id, {
...candidate,
keyword_match: true,
semantic_match: false
});
}
// Add semantic candidates (merge if already exists)
for (const candidate of semanticCandidates) {
if (candidateMap.has(candidate.id)) {
// Merge with existing candidate
const existing = candidateMap.get(candidate.id);
candidateMap.set(candidate.id, {
...existing,
semantic_match: true,
semantic_score: candidate.semantic_score,
matched_terms: [...(existing.matched_terms || []), ...candidate.matched_terms]
});
} else {
// Add new semantic candidate
candidateMap.set(candidate.id, {
...candidate,
keyword_match: false,
semantic_match: true
});
}
}
return Array.from(candidateMap.values());
}
extractSearchTerms(task) {
const text = `${task.title} ${task.description || ''} ${(task.tags || []).join(' ')}`;
// Extract technical terms (CamelCase, UPPERCASE)
const techTerms = [];
const camelCase = text.match(/\b[A-Z][a-z]+(?:[A-Z][a-z]+)*\b/g) || [];
const upperCase = text.match(/\b[A-Z]{2,}\b/g) || [];
techTerms.push(...camelCase, ...upperCase);
// Extract quoted strings
const quoted = [];
const doubleQuoted = text.match(/"([^"]+)"/g) || [];
const singleQuoted = text.match(/'([^']+)'/g) || [];
quoted.push(
...doubleQuoted.map(q => q.replace(/"/g, '')),
...singleQuoted.map(q => q.replace(/'/g, ''))
);
// Extract keywords (remove stop words, short words)
const words = text.toLowerCase()
.replace(/[^\w\s]/g, ' ')
.split(/\s+/)
.filter(word => word.length > 3)
.filter(word => !this.stopWords.has(word));
const keywords = [...new Set(words)];
// Extract specific patterns
const patterns = {
bugs: text.match(/\b(bug|issue|error|problem|fix|broken)\b/gi) || [],
features: text.match(/\b(feature|implement|add|create|build)\b/gi) || [],
improvements: text.match(/\b(improve|enhance|optimize|refactor)\b/gi) || []
};
return {
technical: [...new Set(techTerms)],
quoted: [...new Set(quoted)],
keywords: keywords,
patterns: patterns,
all: [...new Set([...techTerms, ...quoted, ...keywords])]
};
}
async rankByRelevance(memories, task) {
return memories.map(memory => {
let score = 0;
const factors = [];
// Semantic similarity (highest weight if available)
if (memory.semantic_score) {
score += memory.semantic_score * 0.40;
factors.push(`semantic: ${(memory.semantic_score * 100).toFixed(1)}%`);
}
// Project match (adjusted weight)
if (memory.project === task.project) {
score += 0.25;
factors.push('same project');
}
// Category match
if (memory.category === task.category) {
score += 0.15;
factors.push('same category');
}
// Tag overlap
const memoryTags = memory.tags || [];
const taskTags = task.tags || [];
if (memoryTags.length > 0 && taskTags.length > 0) {
const overlap = memoryTags.filter(tag => taskTags.includes(tag)).length;
const tagScore = overlap / Math.max(memoryTags.length, taskTags.length);
score += tagScore * 0.15;
if (overlap > 0) factors.push(`${overlap} common tags`);
}
// Keyword density (reduced weight with semantic search)
const terms = this.extractSearchTerms(task);
const contentLower = memory.content.toLowerCase();
let keywordMatches = 0;
for (const keyword of terms.keywords) {
const regex = new RegExp(`\\b${keyword}\\b`, 'gi');
const matches = (contentLower.match(regex) || []).length;
keywordMatches += matches;
}
if (keywordMatches > 0) {
const keywordScore = Math.min(keywordMatches / 10, 1) * 0.10;
score += keywordScore;
factors.push(`${keywordMatches} keyword matches`);
}
// Technical term matches
let techMatches = 0;
for (const tech of terms.technical) {
if (memory.content.includes(tech)) {
techMatches++;
}
}
if (techMatches > 0) {
score += (techMatches / Math.max(terms.technical.length, 1)) * 0.08;
factors.push(`${techMatches} technical terms`);
}
// Time proximity
const memoryDate = new Date(memory.timestamp);
const taskDate = new Date(task.created);
const daysDiff = Math.abs(taskDate - memoryDate) / (1000 * 60 * 60 * 24);
if (daysDiff <= 1) {
score += 0.08;
factors.push('same day');
} else if (daysDiff <= 7) {
score += 0.06;
factors.push('same week');
} else if (daysDiff <= 14) {
score += 0.04;
factors.push('within 2 weeks');
}
// Content length bonus (longer memories might have more context)
if (memory.content.length > 500) {
score += 0.02;
factors.push('detailed content');
}
// Complexity bonus
if (memory.complexity && memory.complexity >= 3) {
score += 0.02;
factors.push('high complexity');
}
// Hybrid match bonus (found by both keyword and semantic search)
if (memory.keyword_match && memory.semantic_match) {
score += 0.05;
factors.push('hybrid match');
}
return {
...memory,
relevance: Math.min(score, 1.0),
match_factors: factors
};
}).sort((a, b) => b.relevance - a.relevance);
}
determineConnectionType(memory, task) {
const content = memory.content.toLowerCase();
const taskText = `${task.title} ${task.description || ''}`.toLowerCase();
// Check for specific connection types
if (content.includes('research') || content.includes('investigation') ||
content.includes('analysis') || content.includes('study')) {
return 'research';
}
if (content.includes('implement') || content.includes('code') ||
content.includes('function') || content.includes('class')) {
return 'implementation';
}
if (content.includes('bug') || content.includes('fix') ||
content.includes('error') || content.includes('issue')) {
return 'bug_fix';
}
if (content.includes('design') || content.includes('architecture') ||
content.includes('structure') || content.includes('pattern')) {
return 'design';
}
if (content.includes('todo') || content.includes('task') ||
content.includes('plan')) {
return 'planning';
}
if (memory.category === task.category) {
return 'category_match';
}
if (memory.project === task.project) {
return 'project_context';
}
return 'reference';
}
async updateMemoryWithTaskConnection(memoryId, taskConnection) {
const memory = await this.memoryStorage.getMemory(memoryId);
if (!memory) return;
// Initialize task_connections if not present
if (!memory.task_connections) {
memory.task_connections = [];
}
// Avoid duplicate connections
const exists = memory.task_connections.some(tc => tc.task_id === taskConnection.task_id);
if (!exists) {
memory.task_connections.push(taskConnection);
await this.memoryStorage.updateMemory(memory.id, memory);
}
}
async createTaskCompletionMemory(task) {
const linkedMemories = task.memory_connections || [];
const memoryRefs = linkedMemories.map(lm => `- ${lm.memory_id} (${lm.connection_type})`).join('\n');
const completionMemory = {
content: `# Task Completed: ${task.title}
## Task Details
- **ID**: ${task.id}
- **Serial**: ${task.serial}
- **Project**: ${task.project}
- **Category**: ${task.category || 'general'}
- **Priority**: ${task.priority}
- **Created**: ${new Date(task.created).toLocaleDateString()}
- **Completed**: ${new Date().toLocaleDateString()}
## Description
${task.description || 'No description provided'}
## Subtasks
${task.subtasks && task.subtasks.length > 0 ?
task.subtasks.map(st => `- ${st}`).join('\n') :
'No subtasks'}
## Connected Memories
${memoryRefs || 'No connected memories'}
## Lessons Learned
[Add any insights or lessons learned from completing this task]
## Future Improvements
[Note any follow-up tasks or improvements identified]`,
tags: [...(task.tags || []), 'task-completion', 'documentation', task.project],
category: task.category || 'work',
project: task.project,
task_connections: [{
task_id: task.id,
task_serial: task.serial,
connection_type: 'completion_record',
created: new Date().toISOString()
}]
};
return this.memoryStorage.saveMemory(completionMemory);
}
async getTaskContext(taskId, depth = 'direct') {
const task = await this.taskStorage.getTask(taskId);
if (!task) return null;
const context = {
task: task,
direct_memories: [],
related_tasks: [],
related_memories: []
};
// Get directly connected memories
for (const conn of task.memory_connections || []) {
const memory = await this.memoryStorage.getMemory(conn.memory_id);
if (memory) {
context.direct_memories.push({
...memory,
connection: conn
});
}
}
// Get manually linked memories
for (const memoryId of task.manual_memories || []) {
const memory = await this.memoryStorage.getMemory(memoryId);
if (memory) {
context.direct_memories.push({
...memory,
connection: { type: 'manual', relevance: 1.0 }
});
}
}
if (depth === 'deep') {
// Find related tasks (same project, similar tags)
const allTasks = await this.taskStorage.listTasks({ project: task.project });
context.related_tasks = allTasks
.filter(t => t.id !== task.id)
.filter(t => {
const taskTags = new Set(task.tags || []);
const otherTags = new Set(t.tags || []);
const overlap = [...taskTags].filter(tag => otherTags.has(tag));
return overlap.length > 0;
})
.slice(0, 5);
// Find memories from related tasks
const relatedMemoryIds = new Set();
for (const relatedTask of context.related_tasks) {
for (const conn of relatedTask.memory_connections || []) {
relatedMemoryIds.add(conn.memory_id);
}
}
for (const memoryId of relatedMemoryIds) {
const memory = await this.memoryStorage.getMemory(memoryId);
if (memory && !context.direct_memories.some(dm => dm.id === memory.id)) {
context.related_memories.push(memory);
}
}
}
return context;
}
}