@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.
389 lines (338 loc) • 13.3 kB
JavaScript
/**
* Claude-Historian Inspired Features
* Adapts patterns from claude-historian for enhanced search and analysis
*/
export class QueryAnalyzer {
constructor() {
// Intent patterns adapted from claude-historian
this.intentPatterns = {
error: [
/\b(?:error|exception|fail|crash|bug|issue|problem|broken)\b/i,
/\b(?:not working|doesn't work|won't work)\b/i,
/\b(?:troubleshoot|debug|fix)\b/i
],
feature: [
/\b(?:feature|functionality|implement|add|create)\b/i,
/\b(?:how to|how do|can I|is it possible)\b/i,
/\b(?:new|enhancement|improvement)\b/i
],
fix: [
/\b(?:fix|solve|resolve|repair|correct)\b/i,
/\b(?:solution|workaround|patch)\b/i,
/\b(?:working|fixed|resolved|solved)\b/i
],
documentation: [
/\b(?:document|docs|documentation|readme)\b/i,
/\b(?:explain|understand|clarify|meaning)\b/i,
/\b(?:what is|what does|purpose of)\b/i
],
configuration: [
/\b(?:config|configuration|setup|install|deploy)\b/i,
/\b(?:settings|options|parameters)\b/i,
/\b(?:environment|env|variables)\b/i
],
performance: [
/\b(?:performance|speed|slow|fast|optimize)\b/i,
/\b(?:memory|cpu|load|latency)\b/i,
/\b(?:efficiency|benchmark|profile)\b/i
],
testing: [
/\b(?:test|testing|spec|unit|integration)\b/i,
/\b(?:verify|validate|check|ensure)\b/i,
/\b(?:mock|stub|assertion)\b/i
],
deployment: [
/\b(?:deploy|deployment|production|staging)\b/i,
/\b(?:build|compile|package|release)\b/i,
/\b(?:server|hosting|cloud|container)\b/i
]
};
// Common words to filter out from keyword extraction
this.stopWords = new Set([
'the', 'is', 'at', 'which', 'on', 'and', 'a', 'an', 'as', 'are', 'was',
'were', 'been', 'be', 'have', 'has', 'had', 'do', 'does', 'did', 'will',
'would', 'should', 'could', 'may', 'might', 'must', 'can', 'this', 'that',
'these', 'those', 'i', 'you', 'he', 'she', 'it', 'we', 'they', 'what',
'which', 'who', 'when', 'where', 'why', 'how', 'all', 'each', 'every',
'some', 'any', 'many', 'much', 'more', 'most', 'other', 'another', 'such',
'no', 'not', 'only', 'own', 'same', 'so', 'than', 'too', 'very', 'just',
'but', 'for', 'with', 'about', 'from', 'into', 'through', 'during',
'before', 'after', 'above', 'below', 'up', 'down', 'out', 'off', 'over',
'under', 'again', 'further', 'then', 'once'
]);
}
/**
* Analyze query intent (adapted from claude-historian)
*/
analyzeQueryIntent(query) {
const lowerQuery = query.toLowerCase();
const detectedIntents = [];
const keywords = this.extractKeywords(query);
// Check each intent pattern
for (const [intent, patterns] of Object.entries(this.intentPatterns)) {
for (const pattern of patterns) {
if (pattern.test(lowerQuery)) {
detectedIntents.push(intent);
break; // Only count each intent once
}
}
}
// Primary intent is the first detected, or 'general' if none
const primaryIntent = detectedIntents[0] || 'general';
return {
intent: primaryIntent,
allIntents: detectedIntents,
keywords,
needsExpansion: this.shouldExpandQuery(lowerQuery, detectedIntents),
confidence: this.calculateIntentConfidence(detectedIntents, keywords)
};
}
/**
* Extract meaningful keywords from query
*/
extractKeywords(query) {
// Split and clean
const words = query.toLowerCase()
.replace(/[^\w\s-]/g, ' ') // Remove punctuation except hyphens
.split(/\s+/)
.filter(word => word.length > 2)
.filter(word => !this.stopWords.has(word));
// Extract technical terms (CamelCase, acronyms, etc.)
const technicalTerms = query.match(/\b[A-Z][a-zA-Z]*(?:[A-Z][a-z]*)*\b/g) || [];
const acronyms = query.match(/\b[A-Z]{2,}\b/g) || [];
// Combine and dedupe
const allKeywords = [...words, ...technicalTerms.map(t => t.toLowerCase()), ...acronyms.map(a => a.toLowerCase())];
return [...new Set(allKeywords)].slice(0, 10); // Limit to top 10
}
/**
* Determine if query should be expanded
*/
shouldExpandQuery(query, intents) {
// Expand if:
// - Query is very short (< 3 words)
// - Has clear intent but limited keywords
// - Contains technical terms that might have synonyms
const wordCount = query.split(/\s+/).length;
const hasTechnicalTerms = /\b(?:api|sdk|cli|gui|ui|db|sql|json|xml|html|css|js|ts|py|java|go|rust)\b/i.test(query);
return wordCount < 3 || (intents.length > 0 && wordCount < 5) || hasTechnicalTerms;
}
/**
* Calculate confidence in intent detection
*/
calculateIntentConfidence(intents, keywords) {
if (intents.length === 0) return 0.1;
if (intents.length === 1 && keywords.length >= 2) return 0.8;
if (intents.length === 1) return 0.6;
if (intents.length === 2) return 0.7;
return 0.5; // Multiple intents = less confidence
}
/**
* Expand query intelligently (adapted from claude-historian)
* Only adds ONE highly relevant term to avoid search dilution
*/
expandQueryIntelligently(originalQuery, analysis) {
const { intent, keywords } = analysis;
// Intent-specific expansions
const expansions = {
error: ['issue', 'problem', 'debugging', 'troubleshooting'],
fix: ['solution', 'workaround', 'resolved', 'working'],
feature: ['implementation', 'functionality', 'capability'],
documentation: ['guide', 'tutorial', 'explanation', 'reference'],
configuration: ['setup', 'settings', 'installation', 'deployment'],
performance: ['optimization', 'speed', 'efficiency', 'benchmark'],
testing: ['validation', 'verification', 'quality', 'assertion'],
deployment: ['production', 'release', 'distribution', 'packaging']
};
const possibleExpansions = expansions[intent] || [];
// Find expansion term that's not already in the query
const lowerQuery = originalQuery.toLowerCase();
const newTerm = possibleExpansions.find(term =>
!lowerQuery.includes(term) &&
!keywords.some(keyword => keyword.includes(term) || term.includes(keyword))
);
// Only add ONE term to avoid query dilution
if (newTerm) {
return `${originalQuery} ${newTerm}`;
}
return originalQuery;
}
}
export class RelevanceScorer {
constructor() {
// Time decay factors (adapted from claude-historian)
this.timeDecayFactors = {
today: 5, // Last 24 hours
week: 3, // Last 7 days
month: 2, // Last 30 days
older: 1 // Older than 30 days
};
}
/**
* Calculate time-weighted relevance score
*/
calculateTimeDecay(timestamp) {
const now = Date.now();
const ageMs = now - new Date(timestamp).getTime();
const ageDays = ageMs / (1000 * 60 * 60 * 24);
if (ageDays <= 1) return this.timeDecayFactors.today;
if (ageDays <= 7) return this.timeDecayFactors.week;
if (ageDays <= 30) return this.timeDecayFactors.month;
return this.timeDecayFactors.older;
}
/**
* Calculate content richness score
*/
calculateContentScore(memory, query) {
let score = 0;
const content = memory.content.toLowerCase();
const queryLower = query.toLowerCase();
const queryWords = queryLower.split(/\s+/);
// Exact query match bonus
if (content.includes(queryLower)) {
score += 10;
}
// Word overlap scoring
const matchedWords = queryWords.filter(word => content.includes(word));
score += matchedWords.length * 2;
// Content type bonuses
if (memory.metadata?.hasCode) score += 3;
if (memory.metadata?.hasFiles) score += 2;
if (memory.metadata?.hasTools) score += 2;
if (memory.metadata?.hasErrors && /error|fix|debug/.test(queryLower)) score += 4;
// Priority bonus
if (memory.priority === 'high') score += 3;
if (memory.priority === 'medium') score += 1;
// Category relevance
if (memory.category && queryLower.includes(memory.category)) score += 2;
// Tag relevance
if (memory.tags) {
const tagMatches = memory.tags.filter(tag =>
queryWords.some(word => tag.toLowerCase().includes(word))
);
score += tagMatches.length;
}
return score;
}
/**
* Calculate final relevance score combining content and time
*/
calculateFinalScore(memory, query) {
const contentScore = this.calculateContentScore(memory, query);
const timeDecay = this.calculateTimeDecay(memory.created || memory.timestamp);
// Combine scores with time weighting
return Math.round(contentScore * timeDecay * 10) / 10; // Round to 1 decimal
}
/**
* Rank memories by relevance
*/
rankMemories(memories, query) {
return memories
.map(memory => ({
...memory,
relevanceScore: this.calculateFinalScore(memory, query),
timeDecay: this.calculateTimeDecay(memory.created || memory.timestamp)
}))
.sort((a, b) => b.relevanceScore - a.relevanceScore);
}
}
export class ContentClassifier {
constructor() {
// Content detection patterns
this.patterns = {
files: /(?:\/[\w\/\-_.]+\.\w+|\w+\.\w+(?:\s|$))/g,
tools: /\b(?:add_memory|search_memories|create_task|list_tasks|get_memory)\b/g,
errors: /\b(?:error|exception|failed?|crash|bug|issue)\b/i,
code: /```[\s\S]*?```|`[^`]+`/g,
urls: /https?:\/\/[^\s<>"{}|\\^`\[\]]+/g,
commands: /(?:npm|git|node|python|pip|curl|wget)\s+[\w\-]+/g
};
}
/**
* Classify memory content (adapted from claude-historian)
*/
classifyContent(content) {
const classification = {
hasFiles: false,
hasTools: false,
hasErrors: false,
hasCode: false,
hasUrls: false,
hasCommands: false,
contentLength: content.length,
extractedFiles: [],
extractedTools: [],
extractedUrls: [],
extractedCommands: []
};
// Extract and classify content
const fileMatches = content.match(this.patterns.files) || [];
const toolMatches = content.match(this.patterns.tools) || [];
const urlMatches = content.match(this.patterns.urls) || [];
const commandMatches = content.match(this.patterns.commands) || [];
classification.hasFiles = fileMatches.length > 0;
classification.hasTools = toolMatches.length > 0;
classification.hasErrors = this.patterns.errors.test(content);
classification.hasCode = this.patterns.code.test(content);
classification.hasUrls = urlMatches.length > 0;
classification.hasCommands = commandMatches.length > 0;
classification.extractedFiles = [...new Set(fileMatches)].slice(0, 10);
classification.extractedTools = [...new Set(toolMatches)];
classification.extractedUrls = [...new Set(urlMatches)].slice(0, 5);
classification.extractedCommands = [...new Set(commandMatches)].slice(0, 5);
return classification;
}
}
export class CircuitBreaker {
constructor(options = {}) {
this.timeLimit = options.timeLimit || 30000; // 30 seconds
this.memoryLimit = options.memoryLimit || 50000; // 50k items
this.sizeLimit = options.sizeLimit || 100 * 1024; // 100KB per item
}
/**
* Check if operation should be stopped
*/
shouldBreak(startTime, itemCount, itemSize) {
const elapsed = Date.now() - startTime;
if (elapsed > this.timeLimit) {
return { break: true, reason: 'Time limit exceeded' };
}
if (itemCount > this.memoryLimit) {
return { break: true, reason: 'Memory limit exceeded' };
}
if (itemSize > this.sizeLimit) {
return { break: true, reason: 'Item size limit exceeded' };
}
return { break: false };
}
/**
* Execute operation with circuit breaker protection
*/
async execute(operation, items = []) {
const startTime = Date.now();
const results = [];
for (let i = 0; i < items.length; i++) {
const item = items[i];
const itemSize = JSON.stringify(item).length;
// Check circuit breaker
const breakCheck = this.shouldBreak(startTime, i, itemSize);
if (breakCheck.break) {
console.warn(`Circuit breaker triggered: ${breakCheck.reason}`);
break;
}
try {
const result = await operation(item);
results.push(result);
} catch (error) {
console.error(`Operation failed for item ${i}:`, error);
// Continue processing other items
}
}
return results;
}
}
export default {
QueryAnalyzer,
RelevanceScorer,
ContentClassifier,
CircuitBreaker
};