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.
@@ -459,10 +459,14 @@ export class MemoryStore {
459
459
  metadata: {
460
460
  project: row.project,
461
461
  pattern_type: row.pattern_type,
462
+ description: row.description,
462
463
  example: row.example || '',
463
464
  confidence: row.confidence,
464
465
  context: row.context || '',
465
- created_at: row.created_at
466
+ created_at: row.created_at,
467
+ first_seen: row.created_at,
468
+ last_seen: row.created_at,
469
+ occurrences: 1
466
470
  }
467
471
  }))
468
472
  } catch (error) {
@@ -484,14 +488,19 @@ export class MemoryStore {
484
488
  return rows.map(row => ({
485
489
  id: row.id,
486
490
  correction: row.correction,
491
+ description: row.correction,
487
492
  metadata: {
488
493
  project: row.project,
489
494
  original: row.original,
490
495
  correction: row.correction,
496
+ description: row.correction,
491
497
  reasoning: row.reasoning,
492
498
  context: row.context || '',
493
499
  confidence: row.confidence,
494
- created_at: row.created_at
500
+ created_at: row.created_at,
501
+ first_seen: row.created_at,
502
+ last_seen: row.created_at,
503
+ occurrences: 1
495
504
  }
496
505
  }))
497
506
  } catch (error) {
@@ -553,10 +562,14 @@ export class MemoryStore {
553
562
  metadata: {
554
563
  project: row.project,
555
564
  pattern_type: row.pattern_type,
565
+ description: row.description,
556
566
  example: row.example || '',
557
567
  confidence: row.confidence,
558
568
  context: row.context || '',
559
- created_at: row.created_at
569
+ created_at: row.created_at,
570
+ first_seen: row.created_at,
571
+ last_seen: row.created_at,
572
+ occurrences: 1
560
573
  },
561
574
  similarity
562
575
  }
@@ -613,14 +626,19 @@ export class MemoryStore {
613
626
  return {
614
627
  id: row.id,
615
628
  content: row.correction,
629
+ description: row.correction,
616
630
  metadata: {
617
631
  project: row.project,
618
632
  original: row.original,
619
633
  correction: row.correction,
634
+ description: row.correction,
620
635
  reasoning: row.reasoning,
621
636
  context: row.context || '',
622
637
  confidence: row.confidence,
623
- created_at: row.created_at
638
+ created_at: row.created_at,
639
+ first_seen: row.created_at,
640
+ last_seen: row.created_at,
641
+ occurrences: 1
624
642
  },
625
643
  similarity
626
644
  }
@@ -636,6 +654,120 @@ export class MemoryStore {
636
654
  }
637
655
  }
638
656
 
657
+ /**
658
+ * Get all decisions with full content (for analytical tools without ChromaDB)
659
+ */
660
+ getAllDecisionsWithContent(project?: string): any[] {
661
+ try {
662
+ let sql = `
663
+ SELECT d.id, d.context, d.decision, d.reasoning, d.alternatives, d.tags,
664
+ m.content, m.project, m.created_at, m.metadata
665
+ FROM decisions d
666
+ JOIN memories m ON d.memory_id = m.id
667
+ `
668
+ const params: any[] = []
669
+ if (project) {
670
+ sql += ' WHERE m.project = ?'
671
+ params.push(project)
672
+ }
673
+ sql += ' ORDER BY m.created_at DESC'
674
+
675
+ const stmt = this.db.prepare(sql)
676
+ const rows = stmt.all(...params) as any[]
677
+
678
+ return rows.map(row => ({
679
+ id: row.id,
680
+ content: row.content,
681
+ date: row.created_at,
682
+ project: row.project,
683
+ context: row.context,
684
+ decision: row.decision,
685
+ reasoning: row.reasoning,
686
+ alternatives: row.alternatives,
687
+ tags: row.tags ? JSON.parse(row.tags) : []
688
+ }))
689
+ } catch (error) {
690
+ this.logger.error({ error, project }, 'Failed to get all decisions with content')
691
+ return []
692
+ }
693
+ }
694
+
695
+ /**
696
+ * Get all patterns with full content (for analytical tools without ChromaDB)
697
+ */
698
+ getAllPatternsWithContent(project?: string): any[] {
699
+ try {
700
+ let sql = `
701
+ SELECT p.id, p.pattern_type, p.description, p.example, p.confidence, p.context,
702
+ m.content, m.project, m.created_at
703
+ FROM patterns p
704
+ JOIN memories m ON p.memory_id = m.id
705
+ `
706
+ const params: any[] = []
707
+ if (project) {
708
+ sql += ' WHERE p.project = ?'
709
+ params.push(project)
710
+ }
711
+ sql += ' ORDER BY m.created_at DESC'
712
+
713
+ const stmt = this.db.prepare(sql)
714
+ const rows = stmt.all(...params) as any[]
715
+
716
+ return rows.map(row => ({
717
+ id: row.id,
718
+ content: row.content,
719
+ date: row.created_at,
720
+ project: row.project,
721
+ pattern_type: row.pattern_type,
722
+ description: row.description,
723
+ example: row.example,
724
+ confidence: row.confidence,
725
+ context: row.context
726
+ }))
727
+ } catch (error) {
728
+ this.logger.error({ error, project }, 'Failed to get all patterns with content')
729
+ return []
730
+ }
731
+ }
732
+
733
+ /**
734
+ * Get all corrections with full content (for analytical tools without ChromaDB)
735
+ */
736
+ getAllCorrectionsWithContent(project?: string): any[] {
737
+ try {
738
+ let sql = `
739
+ SELECT c.id, c.original, c.correction, c.reasoning, c.context as correction_context,
740
+ c.confidence, m.content, m.project, m.created_at
741
+ FROM corrections c
742
+ JOIN memories m ON c.memory_id = m.id
743
+ `
744
+ const params: any[] = []
745
+ if (project) {
746
+ sql += ' WHERE c.project = ?'
747
+ params.push(project)
748
+ }
749
+ sql += ' ORDER BY m.created_at DESC'
750
+
751
+ const stmt = this.db.prepare(sql)
752
+ const rows = stmt.all(...params) as any[]
753
+
754
+ return rows.map(row => ({
755
+ id: row.id,
756
+ content: row.content,
757
+ date: row.created_at,
758
+ project: row.project,
759
+ original: row.original,
760
+ correction: row.correction,
761
+ reasoning: row.reasoning,
762
+ context: row.correction_context,
763
+ confidence: row.confidence
764
+ }))
765
+ } catch (error) {
766
+ this.logger.error({ error, project }, 'Failed to get all corrections with content')
767
+ return []
768
+ }
769
+ }
770
+
639
771
  /**
640
772
  * Convert database row to Decision object
641
773
  */
@@ -25,12 +25,82 @@ export async function handleAnalyzeDecisionEvolution(
25
25
  const memory = getMemoryService()
26
26
 
27
27
  if (!memory.isChromaDBEnabled()) {
28
- return ResponseFormatter.text(
29
- `No decisions found for topic: "${topic}"\n\n` +
30
- 'Note: ChromaDB is not connected. Decision evolution analysis requires ChromaDB for semantic search. ' +
31
- 'Decisions stored via SQLite fallback are available through recall_similar. ' +
32
- 'To enable evolution tracking, start a ChromaDB server or configure persistent mode.'
33
- )
28
+ // SQLite fallback: use searchRaw for semantic search + chronological analysis
29
+ const results = await memory.searchRaw(topic, {
30
+ project: project_name,
31
+ limit: limit || 20,
32
+ minSimilarity: 0.2
33
+ })
34
+
35
+ if (results.length === 0) {
36
+ return ResponseFormatter.text(`No decisions found for topic: "${topic}"`)
37
+ }
38
+
39
+ // Sort chronologically
40
+ const sorted = results
41
+ .map(r => ({
42
+ date: r.memory?.createdAt?.toISOString?.() || r.memory?.created_at || new Date().toISOString(),
43
+ decision: r.decision?.decision || r.memory?.content || '',
44
+ reasoning: r.decision?.reasoning || '',
45
+ similarity: r.similarity || 0
46
+ }))
47
+ .sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime())
48
+
49
+ // Simple stability assessment based on content similarity between consecutive decisions
50
+ let totalSimilarity = 0
51
+ let comparisons = 0
52
+ const changes: Array<{ daysBetween: number; summary: string }> = []
53
+
54
+ for (let i = 1; i < sorted.length; i++) {
55
+ const prev = sorted[i - 1]
56
+ const curr = sorted[i]
57
+ const daysBetween = Math.round((new Date(curr.date).getTime() - new Date(prev.date).getTime()) / (1000 * 60 * 60 * 24))
58
+
59
+ // Simple Jaccard similarity between word sets
60
+ const prevWords = new Set(prev.decision.toLowerCase().split(/\s+/).filter(w => w.length > 3))
61
+ const currWords = new Set(curr.decision.toLowerCase().split(/\s+/).filter(w => w.length > 3))
62
+ const intersection = new Set([...prevWords].filter(w => currWords.has(w)))
63
+ const union = new Set([...prevWords, ...currWords])
64
+ const jaccard = union.size > 0 ? intersection.size / union.size : 0
65
+ totalSimilarity += jaccard
66
+ comparisons++
67
+
68
+ if (jaccard < 0.5) {
69
+ changes.push({
70
+ daysBetween,
71
+ summary: `Changed from "${prev.decision.slice(0, 60)}..." to "${curr.decision.slice(0, 60)}..."`
72
+ })
73
+ }
74
+ }
75
+
76
+ const avgSimilarity = comparisons > 0 ? totalSimilarity / comparisons : 1
77
+ const stability = avgSimilarity > 0.7 ? 'stable' : avgSimilarity > 0.4 ? 'evolving' : 'volatile'
78
+ const currentState = sorted[sorted.length - 1].decision
79
+
80
+ const parts: string[] = []
81
+ parts.push(`## Decision Evolution: "${topic}" (SQLite)`)
82
+ if (project_name) parts.push(`**Project:** ${project_name}`)
83
+ parts.push(`**Total decisions:** ${sorted.length}`)
84
+ parts.push(`**Stability:** ${stability}`)
85
+ parts.push(`**Current state:** ${currentState.slice(0, 200)}`)
86
+ parts.push('')
87
+
88
+ if (changes.length > 0) {
89
+ parts.push('### Changes Over Time')
90
+ for (const change of changes) {
91
+ parts.push(`- **${change.daysBetween} days apart:** ${change.summary}`)
92
+ }
93
+ parts.push('')
94
+ }
95
+
96
+ parts.push('### Timeline')
97
+ for (const entry of sorted.slice(0, 10)) {
98
+ const date = new Date(entry.date).toLocaleDateString()
99
+ parts.push(`- **${date}:** ${entry.decision.slice(0, 150)}`)
100
+ }
101
+
102
+ const statsLine = formatMemoryStats({ recalled: sorted.length })
103
+ return ResponseFormatter.text(withMemoryIndicator(parts.join('\n'), sorted.length) + '\n\n' + statsLine)
34
104
  }
35
105
 
36
106
  const tracker = new DecisionEvolutionTracker(
@@ -25,12 +25,66 @@ export async function handleDetectTrends(
25
25
  const memory = getMemoryService()
26
26
 
27
27
  if (!memory.isChromaDBEnabled()) {
28
- return ResponseFormatter.text(
29
- 'No decisions found for trend analysis.\n\n' +
30
- 'Note: ChromaDB is not connected. Trend detection requires ChromaDB for semantic search across decisions. ' +
31
- 'Decisions stored via SQLite fallback are available through recall_similar and get_patterns. ' +
32
- 'To enable trend detection, start a ChromaDB server or configure persistent mode.'
33
- )
28
+ // SQLite fallback: frequency-based trend analysis
29
+ const decisions = await memory.fetchAllDecisions(project_name)
30
+
31
+ if (decisions.length === 0) {
32
+ return ResponseFormatter.text('No decisions found for trend analysis.')
33
+ }
34
+
35
+ // Filter by period
36
+ const cutoffDate = new Date()
37
+ cutoffDate.setDate(cutoffDate.getDate() - (period_days || 90))
38
+ const filtered = decisions.filter(d => new Date(d.date) >= cutoffDate)
39
+
40
+ if (filtered.length === 0) {
41
+ return ResponseFormatter.text(`No decisions found in the last ${period_days || 90} days.`)
42
+ }
43
+
44
+ // Extract term frequencies from decision content
45
+ const stopWords = new Set(['the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could', 'should', 'may', 'might', 'can', 'shall', 'to', 'of', 'in', 'for', 'on', 'with', 'at', 'by', 'from', 'as', 'into', 'through', 'during', 'before', 'after', 'above', 'below', 'between', 'out', 'off', 'over', 'under', 'again', 'further', 'then', 'once', 'and', 'but', 'or', 'nor', 'not', 'so', 'yet', 'both', 'either', 'neither', 'each', 'every', 'all', 'any', 'few', 'more', 'most', 'other', 'some', 'such', 'no', 'only', 'own', 'same', 'than', 'too', 'very', 'just', 'because', 'if', 'when', 'where', 'how', 'what', 'which', 'who', 'whom', 'this', 'that', 'these', 'those', 'i', 'me', 'my', 'we', 'our', 'you', 'your', 'he', 'him', 'his', 'she', 'her', 'it', 'its', 'they', 'them', 'their', 'use', 'using', 'used', 'decision', 'context', 'reasoning'])
46
+ const termCounts: Record<string, { count: number; projects: Set<string> }> = {}
47
+
48
+ for (const d of filtered) {
49
+ const text = `${d.decision || ''} ${d.context || ''} ${d.reasoning || ''}`.toLowerCase()
50
+ const words = text.match(/\b[a-z][a-z-]{2,}\b/g) || []
51
+ const seen = new Set<string>()
52
+ for (const word of words) {
53
+ if (stopWords.has(word) || seen.has(word)) continue
54
+ seen.add(word)
55
+ if (!termCounts[word]) termCounts[word] = { count: 0, projects: new Set() }
56
+ termCounts[word].count++
57
+ if (d.project) termCounts[word].projects.add(d.project)
58
+ }
59
+ }
60
+
61
+ const minOcc = min_occurrences || 2
62
+ const trends = Object.entries(termCounts)
63
+ .filter(([, v]) => v.count >= minOcc)
64
+ .map(([term, v]) => ({ term, occurrences: v.count, projects: Array.from(v.projects) }))
65
+ .sort((a, b) => b.occurrences - a.occurrences)
66
+ .slice(0, limit || 20)
67
+
68
+ const parts: string[] = []
69
+ parts.push(`## Trend Analysis (SQLite)`)
70
+ if (project_name) parts.push(`**Project:** ${project_name}`)
71
+ parts.push(`**Period:** Last ${period_days || 90} days`)
72
+ parts.push(`**Decisions analyzed:** ${filtered.length}`)
73
+ parts.push('')
74
+
75
+ if (trends.length > 0) {
76
+ parts.push('### Top Terms by Frequency')
77
+ for (const t of trends) {
78
+ parts.push(`- **${t.term}** (${t.occurrences}x, projects: ${t.projects.join(', ')})`)
79
+ }
80
+ } else {
81
+ parts.push('No significant trends found with current filters.')
82
+ }
83
+
84
+ parts.push('\n*Note: SQLite mode provides frequency-based analysis. Enable ChromaDB for momentum and rising/declining classification.*')
85
+
86
+ const statsLine = formatMemoryStats({ recalled: filtered.length })
87
+ return ResponseFormatter.text(withMemoryIndicator(parts.join('\n'), filtered.length) + '\n\n' + statsLine)
34
88
  }
35
89
 
36
90
  const detector = new TrendDetector(logger, memory.chroma.collections)
@@ -25,12 +25,97 @@ export async function handleFindCrossProjectPatterns(
25
25
  const memory = getMemoryService()
26
26
 
27
27
  if (!memory.isChromaDBEnabled()) {
28
- return ResponseFormatter.text(
29
- 'No cross-project patterns found.\n\n' +
30
- 'Note: ChromaDB is not connected. Cross-project pattern analysis requires ChromaDB for semantic search across decisions. ' +
31
- 'Decisions stored via SQLite fallback are available through recall_similar and get_patterns. ' +
32
- 'To enable cross-project analysis, start a ChromaDB server or configure persistent mode.'
33
- )
28
+ // SQLite fallback: fetch all decisions, group by project, find shared terms
29
+ const decisions = await memory.fetchAllDecisions()
30
+
31
+ if (decisions.length === 0) {
32
+ return ResponseFormatter.text('No cross-project patterns found (no decisions in database).')
33
+ }
34
+
35
+ // Group by project
36
+ const byProject: Record<string, any[]> = {}
37
+ for (const d of decisions) {
38
+ const proj = d.project || 'unknown'
39
+ if (!byProject[proj]) byProject[proj] = []
40
+ byProject[proj].push(d)
41
+ }
42
+
43
+ const projectNames = Object.keys(byProject)
44
+ const minProj = min_projects || 2
45
+
46
+ if (projectNames.length < minProj) {
47
+ return ResponseFormatter.text(
48
+ `Only ${projectNames.length} project(s) found (${projectNames.join(', ')}). Need at least ${minProj} for cross-project analysis.`
49
+ )
50
+ }
51
+
52
+ // Extract significant terms per project
53
+ const stopWords = new Set(['the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could', 'should', 'to', 'of', 'in', 'for', 'on', 'with', 'at', 'by', 'from', 'as', 'and', 'but', 'or', 'not', 'so', 'if', 'when', 'where', 'how', 'what', 'which', 'who', 'this', 'that', 'use', 'using', 'used', 'decision', 'context', 'reasoning'])
54
+ const termProjects: Record<string, { projects: Set<string>; examples: Array<{ project: string; content: string }> }> = {}
55
+
56
+ for (const [proj, decs] of Object.entries(byProject)) {
57
+ const projectTerms = new Set<string>()
58
+ for (const d of decs) {
59
+ const text = `${d.decision || ''} ${d.context || ''}`.toLowerCase()
60
+ const words = text.match(/\b[a-z][a-z-]{2,}\b/g) || []
61
+ for (const word of words) {
62
+ if (stopWords.has(word)) continue
63
+ if (!projectTerms.has(word)) {
64
+ projectTerms.add(word)
65
+ if (!termProjects[word]) termProjects[word] = { projects: new Set(), examples: [] }
66
+ termProjects[word].projects.add(proj)
67
+ if (termProjects[word].examples.length < 3) {
68
+ termProjects[word].examples.push({ project: proj, content: d.decision?.slice(0, 100) || d.content?.slice(0, 100) || '' })
69
+ }
70
+ }
71
+ }
72
+ }
73
+ }
74
+
75
+ // Filter terms that appear in >= minProj projects
76
+ const crossPatterns = Object.entries(termProjects)
77
+ .filter(([, v]) => v.projects.size >= minProj)
78
+ .map(([term, v]) => ({
79
+ description: term,
80
+ projects: Array.from(v.projects),
81
+ occurrences: v.projects.size,
82
+ confidence: v.projects.size / projectNames.length,
83
+ examples: v.examples
84
+ }))
85
+ .sort((a, b) => b.occurrences - a.occurrences)
86
+ .slice(0, limit || 20)
87
+
88
+ if (crossPatterns.length === 0) {
89
+ return ResponseFormatter.text(
90
+ `No cross-project patterns found (analyzed ${decisions.length} decisions across ${projectNames.length} projects).`
91
+ )
92
+ }
93
+
94
+ const parts: string[] = []
95
+ parts.push(`## Cross-Project Patterns (SQLite)`)
96
+ parts.push(`**Projects analyzed:** ${projectNames.join(', ')}`)
97
+ parts.push(`**Decisions analyzed:** ${decisions.length}`)
98
+ parts.push(`**Shared terms found:** ${crossPatterns.length}`)
99
+ parts.push('')
100
+
101
+ for (const pattern of crossPatterns) {
102
+ const confidence = Math.round(pattern.confidence * 100)
103
+ parts.push(`### ${pattern.description} (${confidence}% confidence)`)
104
+ parts.push(`**Projects:** ${pattern.projects.join(', ')}`)
105
+ parts.push(`**Occurrences:** ${pattern.occurrences} projects`)
106
+ if (pattern.examples.length > 0) {
107
+ parts.push('**Examples:**')
108
+ for (const ex of pattern.examples.slice(0, 3)) {
109
+ parts.push(`- [${ex.project}] ${ex.content}`)
110
+ }
111
+ }
112
+ parts.push('')
113
+ }
114
+
115
+ parts.push('*Note: SQLite mode uses term-frequency analysis. Enable ChromaDB for semantic pattern generalization.*')
116
+
117
+ const statsLine = formatMemoryStats({ patterns: crossPatterns.length })
118
+ return ResponseFormatter.text(withMemoryIndicator(parts.join('\n'), crossPatterns.length) + '\n\n' + statsLine)
34
119
  }
35
120
 
36
121
  const generalizer = new PatternGeneralizer(
@@ -25,21 +25,6 @@ export async function handleGetDecisionTimeline(
25
25
 
26
26
  const memory = getMemoryService()
27
27
 
28
- if (!memory.isChromaDBEnabled()) {
29
- return ResponseFormatter.text(
30
- 'No decisions found for the specified criteria.\n\n' +
31
- 'Note: ChromaDB is not connected. Decision timeline requires ChromaDB for semantic search. ' +
32
- 'Decisions stored via SQLite fallback are available through recall_similar. ' +
33
- 'To enable timeline view, start a ChromaDB server or configure persistent mode.'
34
- )
35
- }
36
-
37
- const timelineBuilder = new TimelineBuilder(
38
- logger,
39
- memory.chroma.collections,
40
- memory.chroma.embeddings
41
- )
42
-
43
28
  // Parse temporal expression if provided
44
29
  let startDate: string | undefined
45
30
  let endDate: string | undefined
@@ -51,6 +36,97 @@ export async function handleGetDecisionTimeline(
51
36
  endDate = parsed.endDate
52
37
  }
53
38
 
39
+ if (!memory.isChromaDBEnabled()) {
40
+ // SQLite fallback: fetch all data and build timeline
41
+ const [decisions, patterns, corrections] = await Promise.all([
42
+ memory.fetchAllDecisions(project_name),
43
+ memory.fetchAllPatterns(project_name),
44
+ memory.fetchAllCorrections(project_name)
45
+ ])
46
+
47
+ // Combine into timeline entries
48
+ let entries: Array<{ date: string; type: string; content: string; reasoning?: string }> = []
49
+
50
+ for (const d of decisions) {
51
+ entries.push({
52
+ date: d.date,
53
+ type: 'decision',
54
+ content: d.decision || d.content,
55
+ reasoning: d.reasoning
56
+ })
57
+ }
58
+ for (const p of patterns) {
59
+ entries.push({
60
+ date: p.date,
61
+ type: 'pattern',
62
+ content: p.description || p.content
63
+ })
64
+ }
65
+ for (const c of corrections) {
66
+ entries.push({
67
+ date: c.date,
68
+ type: 'correction',
69
+ content: c.correction || c.content,
70
+ reasoning: c.reasoning
71
+ })
72
+ }
73
+
74
+ // Filter by topic (substring match)
75
+ if (topic) {
76
+ const topicLower = topic.toLowerCase()
77
+ entries = entries.filter(e =>
78
+ e.content.toLowerCase().includes(topicLower) ||
79
+ (e.reasoning && e.reasoning.toLowerCase().includes(topicLower))
80
+ )
81
+ }
82
+
83
+ // Filter by date range
84
+ if (startDate) {
85
+ entries = entries.filter(e => e.date >= startDate!)
86
+ }
87
+ if (endDate) {
88
+ entries = entries.filter(e => e.date <= endDate!)
89
+ }
90
+
91
+ // Sort chronologically and limit
92
+ entries.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime())
93
+ const limited = entries.slice(0, limit || 20)
94
+
95
+ if (limited.length === 0) {
96
+ return ResponseFormatter.text('No decisions found for the specified criteria.')
97
+ }
98
+
99
+ const parts: string[] = []
100
+ parts.push(`## Decision Timeline (SQLite)`)
101
+ if (project_name) parts.push(`**Project:** ${project_name}`)
102
+ if (topic) parts.push(`**Topic:** ${topic}`)
103
+ if (limited.length > 0) {
104
+ parts.push(`**Period:** ${new Date(limited[0].date).toLocaleDateString()} - ${new Date(limited[limited.length - 1].date).toLocaleDateString()}`)
105
+ }
106
+ parts.push(`**Total entries:** ${limited.length}`)
107
+ parts.push('')
108
+
109
+ for (const entry of limited) {
110
+ const date = new Date(entry.date).toLocaleDateString()
111
+ const icon = entry.type === 'decision' ? '📋' : entry.type === 'pattern' ? '🔄' : '⚠️'
112
+ parts.push(`### ${icon} ${date} [${entry.type}]`)
113
+ parts.push(entry.content.slice(0, 200))
114
+ if (entry.reasoning) {
115
+ parts.push(`*Reasoning:* ${entry.reasoning.slice(0, 100)}`)
116
+ }
117
+ parts.push('')
118
+ }
119
+
120
+ const statsLine = formatMemoryStats({ recalled: limited.length })
121
+ return ResponseFormatter.text(withMemoryIndicator(parts.join('\n'), limited.length) + '\n\n' + statsLine)
122
+ }
123
+
124
+ const timelineBuilder = new TimelineBuilder(
125
+ logger,
126
+ memory.chroma.collections,
127
+ memory.chroma.embeddings
128
+ )
129
+
54
130
  const timeline = await timelineBuilder.buildTimeline({
55
131
  project: project_name,
56
132
  topic,
@@ -32,7 +32,17 @@ export async function handleGetEpisode(
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