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.
- package/VERSION +1 -1
- package/package.json +1 -1
- package/src/cli/bin.ts +12 -6
- package/src/cli/commands/chroma.ts +182 -16
- package/src/cli/commands/serve.ts +6 -0
- package/src/cli/commands/start.ts +42 -0
- package/src/config/defaults.ts +1 -1
- package/src/config/schema.ts +1 -1
- package/src/memory/chroma/client.ts +6 -2
- package/src/memory/chroma/index.ts +1 -1
- package/src/memory/chroma/store.ts +27 -8
- package/src/memory/index.ts +91 -0
- package/src/memory/store.ts +136 -4
- package/src/server/handlers/tools/analyze-decision-evolution.ts +76 -6
- package/src/server/handlers/tools/detect-trends.ts +60 -6
- package/src/server/handlers/tools/find-cross-project-patterns.ts +91 -6
- package/src/server/handlers/tools/get-decision-timeline.ts +91 -15
- package/src/server/handlers/tools/get-episode.ts +11 -1
- package/src/server/handlers/tools/get-recommendations.ts +76 -6
- package/src/server/handlers/tools/list-episodes.ts +11 -1
- package/src/server/handlers/tools/rate-memory.ts +8 -2
- package/src/server/handlers/tools/smart-context.ts +23 -1
- package/src/server/handlers/tools/update-progress.ts +18 -13
- package/src/server/handlers/tools/what-if-analysis.ts +58 -7
package/src/memory/store.ts
CHANGED
|
@@ -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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
'
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
'
|
|
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
|
|
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
|
|