claude-brain 0.4.1 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -25,12 +25,82 @@ export async function handleGetRecommendations(
25
25
  const memory = getMemoryService()
26
26
 
27
27
  if (!memory.isChromaDBEnabled()) {
28
- return ResponseFormatter.text(
29
- `No recommendations found for: "${query}"\n\n` +
30
- 'Note: ChromaDB is not connected. Recommendations require ChromaDB for semantic search across patterns, corrections, and decisions. ' +
31
- 'Decisions stored via SQLite fallback are available through recall_similar, get_patterns, and get_corrections. ' +
32
- 'To enable recommendations, start a ChromaDB server or configure persistent mode.'
33
- )
28
+ // SQLite fallback: combine searchRaw + searchPatterns + searchCorrections
29
+ const [rawResults, patternResults, correctionResults] = await Promise.all([
30
+ memory.searchRaw(query, { project: project_name, limit: limit || 5, minSimilarity: 0.2 }),
31
+ memory.searchPatterns(query, { project: project_name, limit: limit || 5, minSimilarity: 0.2 }),
32
+ memory.searchCorrections(query, { project: project_name, limit: limit || 5, minSimilarity: 0.2 })
33
+ ])
34
+
35
+ // Combine and format as recommendations
36
+ const recommendations: Array<{ type: string; source: string; content: string; reasoning: string; confidence: number }> = []
37
+
38
+ // Corrections get highest priority (lessons learned)
39
+ for (const c of correctionResults) {
40
+ recommendations.push({
41
+ type: 'correction',
42
+ source: 'Past Correction',
43
+ content: `Original: ${c.metadata?.original || ''}\nCorrection: ${c.metadata?.correction || c.content || ''}`,
44
+ reasoning: c.metadata?.reasoning || 'Based on previous lesson learned',
45
+ confidence: c.similarity || c.metadata?.confidence || 0.5
46
+ })
47
+ }
48
+
49
+ // Patterns
50
+ for (const p of patternResults) {
51
+ recommendations.push({
52
+ type: p.metadata?.pattern_type || 'pattern',
53
+ source: `Pattern (${p.metadata?.pattern_type || 'general'})`,
54
+ content: p.metadata?.description || p.content || '',
55
+ reasoning: p.metadata?.context || 'Based on recognized pattern',
56
+ confidence: p.similarity || p.metadata?.confidence || 0.5
57
+ })
58
+ }
59
+
60
+ // Decisions
61
+ for (const r of rawResults) {
62
+ recommendations.push({
63
+ type: 'decision',
64
+ source: 'Past Decision',
65
+ content: r.decision?.decision || r.memory?.content?.slice(0, 300) || '',
66
+ reasoning: r.decision?.reasoning || 'Based on similar past decision',
67
+ confidence: r.similarity || 0.5
68
+ })
69
+ }
70
+
71
+ // Sort by confidence, deduplicate, and limit
72
+ const seen = new Set<string>()
73
+ const deduped = recommendations.filter(r => {
74
+ const key = r.content.slice(0, 50)
75
+ if (seen.has(key)) return false
76
+ seen.add(key)
77
+ return true
78
+ })
79
+ .sort((a, b) => b.confidence - a.confidence)
80
+ .slice(0, limit || 5)
81
+
82
+ if (deduped.length === 0) {
83
+ return ResponseFormatter.text(`No recommendations found for: "${query}"`)
84
+ }
85
+
86
+ const parts: string[] = []
87
+ parts.push(`## Recommendations (SQLite)`)
88
+ parts.push(`**Query:** ${query}`)
89
+ if (project_name) parts.push(`**Project:** ${project_name}`)
90
+ parts.push(`**Found:** ${deduped.length}`)
91
+ parts.push('')
92
+
93
+ for (const rec of deduped) {
94
+ const confidence = Math.round(rec.confidence * 100)
95
+ const icon = rec.type === 'correction' ? '⚠️' : rec.type === 'best-practice' ? '✅' : rec.type === 'pattern' ? '🔄' : '📋'
96
+ parts.push(`### ${icon} ${rec.source} (${confidence}% confidence)`)
97
+ parts.push(rec.content.slice(0, 300))
98
+ parts.push(`*${rec.reasoning}*`)
99
+ parts.push('')
100
+ }
101
+
102
+ const statsLine = formatMemoryStats({ recalled: deduped.length })
103
+ return ResponseFormatter.text(withMemoryIndicator(parts.join('\n'), deduped.length) + '\n\n' + statsLine)
34
104
  }
35
105
 
36
106
  const recommender = new Recommender(
@@ -32,7 +32,17 @@ export async function handleListEpisodes(
32
32
  const episodeService = getEpisodeService()
33
33
  if (!episodeService) {
34
34
  return ResponseFormatter.text(
35
- 'Episodic memory is not enabled. Enable it by setting knowledge.episodic.enabled=true in config.'
35
+ '## Episodic Memory Not Available\n\n' +
36
+ 'Episodic memory requires ChromaDB to track conversation sessions.\n\n' +
37
+ '**To enable:**\n' +
38
+ '- Start a ChromaDB server: `chroma run --path /path/to/data`\n' +
39
+ '- Set `CHROMA_URL=http://localhost:8000` in your environment\n' +
40
+ '- Set `knowledge.episodic.enabled=true` in config\n\n' +
41
+ '**Alternative tools that work without ChromaDB:**\n' +
42
+ '- `recall_similar` — search past decisions by semantic similarity\n' +
43
+ '- `get_patterns` — retrieve recognized patterns\n' +
44
+ '- `get_corrections` — retrieve lessons learned\n' +
45
+ '- `smart_context` — get full project context with memory recall'
36
46
  )
37
47
  }
38
48
 
@@ -39,8 +39,14 @@ export async function handleRateMemory(
39
39
  // Check if feedback is enabled
40
40
  if (!retrieval) {
41
41
  return ResponseFormatter.text(
42
- 'Feedback collection is not enabled. ' +
43
- 'Enable it by setting RETRIEVAL_FEEDBACK_ENABLED=true or retrieval.feedback.enabled=true in config.'
42
+ '## Memory Feedback Not Available\n\n' +
43
+ 'Memory rating requires the retrieval feedback system, which depends on ChromaDB.\n\n' +
44
+ '**To enable:**\n' +
45
+ '- Start a ChromaDB server: `chroma run --path /path/to/data`\n' +
46
+ '- Set `CHROMA_URL=http://localhost:8000` in your environment\n' +
47
+ '- Set `RETRIEVAL_FEEDBACK_ENABLED=true` or `retrieval.feedback.enabled=true` in config\n\n' +
48
+ '**Note:** Your rating was not lost — the memory system will still function without feedback. ' +
49
+ 'Feedback helps improve retrieval quality over time by learning which memories are most useful.'
44
50
  )
45
51
  }
46
52
 
@@ -5,7 +5,7 @@
5
5
 
6
6
  import type { Logger } from 'pino'
7
7
  import type { ToolResponse } from '@/tools/types'
8
- import { getContextService, getPhase12Service, isServicesInitialized } from '@/server/services'
8
+ import { getContextService, getPhase12Service, getMemoryService, isServicesInitialized } from '@/server/services'
9
9
  import { ToolValidator } from '@/server/utils/validators'
10
10
  import { ResponseFormatter } from '@/server/utils/response-formatter'
11
11
  import { withMemoryIndicator, formatMemoryStats } from '@/server/utils/memory-indicator'
@@ -52,6 +52,28 @@ export async function handleSmartContext(
52
52
  // Process with Phase 12 for proactive recall
53
53
  const phase12Result = await phase12.processMessage(current_task, project_name)
54
54
 
55
+ // If Phase12's gated recall found nothing, do direct search (like recall_similar)
56
+ if (!phase12Result.recalledMemories || phase12Result.recalledMemories.memories.length === 0) {
57
+ try {
58
+ const memory = getMemoryService()
59
+ const directRecall = await memory.searchRaw(current_task, {
60
+ project: project_name,
61
+ limit: 5,
62
+ minSimilarity: min_similarity || 0.3
63
+ })
64
+ if (directRecall.length > 0) {
65
+ phase12Result.recalledMemories = {
66
+ query: current_task,
67
+ memories: directRecall,
68
+ relevanceScore: directRecall.reduce((sum: number, m: any) => sum + (m.similarity || 0), 0) / directRecall.length,
69
+ triggeredBy: ['direct-search-fallback']
70
+ }
71
+ }
72
+ } catch (e) {
73
+ logger.debug({ error: e }, 'Direct recall fallback failed, continuing without')
74
+ }
75
+ }
76
+
55
77
  // Format base context
56
78
  const formattedContext = contextService.formatter.format(context)
57
79
 
@@ -43,6 +43,7 @@ export async function handleUpdateProgress(
43
43
 
44
44
  const taskId = generateTaskId(completed_task)
45
45
 
46
+ // Step 1: Append completed task to progress file
46
47
  await context.progress.addCompletedTask(project_name, {
47
48
  id: taskId,
48
49
  title: completed_task,
@@ -50,19 +51,8 @@ export async function handleUpdateProgress(
50
51
  completedAt: new Date()
51
52
  })
52
53
 
53
- // Calculate and update completion percentage
54
- const progressState = await context.progress.getProgress(project_name)
55
- const totalTasks = progressState.completedTasks.length + progressState.currentTasks.length
56
- const completionPercentage = totalTasks > 0
57
- ? Math.round((progressState.completedTasks.length / totalTasks) * 100)
58
- : 0
59
-
60
- await context.progress.updateProgress(project_name, {
61
- completionPercentage,
62
- currentPhase: completionPercentage >= 100 ? 'complete' : 'active'
63
- })
64
-
65
- const progressFile = await vault.reader.readMarkdownFile(projectPaths.progress)
54
+ // Step 2: Read file bypassing cache, update next steps, write content
55
+ const progressFile = await vault.reader.readMarkdownFile(projectPaths.progress, false)
66
56
  const updatedContent = updateNextStepsSection(progressFile.content, next_steps)
67
57
 
68
58
  await vault.writer.writeMarkdownFile(
@@ -72,11 +62,26 @@ export async function handleUpdateProgress(
72
62
  true
73
63
  )
74
64
 
65
+ // Step 3: Notes appended if present
75
66
  if (notes) {
76
67
  const notesEntry = `\n## Notes (${new Date().toLocaleDateString()})\n${notes}\n`
77
68
  await vault.writer.appendContent(projectPaths.progress, notesEntry)
78
69
  }
79
70
 
71
+ // Step 4: Calculate and update frontmatter LAST (so nothing overwrites it)
72
+ const progressState = await context.progress.getProgress(project_name)
73
+ const totalTasks = progressState.completedTasks.length + progressState.currentTasks.length
74
+ const completionPercentage = totalTasks > 0
75
+ ? Math.round((progressState.completedTasks.length / totalTasks) * 100)
76
+ : 0
77
+
78
+ await context.progress.updateProgress(project_name, {
79
+ completionPercentage,
80
+ currentPhase: completionPercentage >= 100 ? 'complete' : 'active'
81
+ })
82
+
83
+ // Step 5: Invalidate cache so subsequent reads get fresh data
84
+ vault.reader.invalidateCache(projectPaths.progress)
80
85
  context.invalidateContext(project_name)
81
86
 
82
87
  logger.info({ projectName: project_name, taskId }, 'Progress updated successfully')
@@ -25,13 +25,64 @@ export async function handleWhatIfAnalysis(
25
25
  const memory = getMemoryService()
26
26
 
27
27
  if (!memory.isChromaDBEnabled()) {
28
- return ResponseFormatter.text(
29
- `## What-If: "${change}"\n\n` +
30
- 'Unable to perform what-if analysis.\n\n' +
31
- 'Note: ChromaDB is not connected. What-if analysis requires ChromaDB for semantic search across decisions. ' +
32
- 'Decisions stored via SQLite fallback are available through recall_similar. ' +
33
- 'To enable what-if analysis, start a ChromaDB server or configure persistent mode.'
34
- )
28
+ // SQLite fallback: use searchRaw for semantic search + knowledge graph if available
29
+ const results = await memory.searchRaw(change, {
30
+ project: project_name,
31
+ limit: max_results || 10,
32
+ minSimilarity: 0.2
33
+ })
34
+
35
+ const parts: string[] = []
36
+ parts.push(`## What-If: "${change}" (SQLite)`)
37
+ parts.push('')
38
+
39
+ if (results.length === 0) {
40
+ parts.push('No related decisions found to assess impact.')
41
+ return ResponseFormatter.text(parts.join('\n'))
42
+ }
43
+
44
+ // Assess impact based on similarity
45
+ const affectedDecisions = results.map(r => {
46
+ const similarity = r.similarity || 0
47
+ const impact = similarity > 0.7 ? 'high' : similarity > 0.4 ? 'medium' : 'low'
48
+ return {
49
+ decision: r.decision?.decision || r.memory?.content?.slice(0, 150) || '',
50
+ impact,
51
+ similarity,
52
+ reason: `${Math.round(similarity * 100)}% similarity — ${r.decision?.reasoning?.slice(0, 100) || 'related context found'}`
53
+ }
54
+ })
55
+
56
+ parts.push(`Analyzed ${results.length} related decisions.\n`)
57
+
58
+ parts.push('### Affected Decisions')
59
+ for (const d of affectedDecisions) {
60
+ const icon = d.impact === 'high' ? '🔴' : d.impact === 'medium' ? '🟡' : '🟢'
61
+ parts.push(`${icon} **[${d.impact}]** ${d.decision}`)
62
+ parts.push(` *${d.reason}*`)
63
+ }
64
+
65
+ // Try knowledge graph if available
66
+ try {
67
+ const kgService = getKnowledgeGraphService()
68
+ if (kgService?.graph) {
69
+ const graphResults = await kgService.graph.search(change, { limit: 5 })
70
+ if (graphResults && graphResults.length > 0) {
71
+ const technologies = graphResults
72
+ .filter((n: any) => n.type === 'technology')
73
+ .map((n: any) => n.name || n.label)
74
+ if (technologies.length > 0) {
75
+ parts.push(`\n### Connected Technologies`)
76
+ parts.push(technologies.map((t: string) => `- ${t}`).join('\n'))
77
+ }
78
+ }
79
+ }
80
+ } catch {
81
+ // Knowledge graph not available, skip
82
+ }
83
+
84
+ const content = parts.join('\n')
85
+ return ResponseFormatter.text(withMemoryIndicator(content, results.length))
35
86
  }
36
87
 
37
88
  const kgService = getKnowledgeGraphService()