claude-brain 0.5.1 → 0.8.0

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.
Files changed (43) hide show
  1. package/VERSION +1 -1
  2. package/assets/CLAUDE-unified.md +11 -0
  3. package/package.json +2 -1
  4. package/packs/backend/node.json +173 -0
  5. package/packs/core/javascript.json +176 -0
  6. package/packs/core/typescript.json +222 -0
  7. package/packs/frontend/react.json +254 -0
  8. package/packs/meta/testing.json +172 -0
  9. package/src/cli/bin.ts +14 -0
  10. package/src/cli/commands/hooks.ts +214 -0
  11. package/src/cli/commands/pack.ts +197 -0
  12. package/src/cli/commands/serve.ts +34 -0
  13. package/src/config/defaults.ts +1 -1
  14. package/src/config/schema.ts +85 -2
  15. package/src/hooks/brain-hook.ts +110 -0
  16. package/src/hooks/capture.ts +161 -0
  17. package/src/hooks/deduplicator.ts +72 -0
  18. package/src/hooks/index.ts +19 -0
  19. package/src/hooks/installer.ts +181 -0
  20. package/src/hooks/passive-classifier.ts +366 -0
  21. package/src/hooks/queue.ts +122 -0
  22. package/src/hooks/session-tracker.ts +199 -0
  23. package/src/hooks/types.ts +47 -0
  24. package/src/memory/chroma/store.ts +2 -1
  25. package/src/memory/index.ts +1 -0
  26. package/src/memory/store.ts +1 -0
  27. package/src/packs/index.ts +9 -0
  28. package/src/packs/loader.ts +134 -0
  29. package/src/packs/manager.ts +204 -0
  30. package/src/packs/ranker.ts +78 -0
  31. package/src/packs/types.ts +81 -0
  32. package/src/routing/entity-extractor.ts +410 -0
  33. package/src/routing/intent-classifier.ts +229 -0
  34. package/src/routing/response-filter.ts +221 -0
  35. package/src/routing/router.ts +671 -0
  36. package/src/server/handlers/call-tool.ts +7 -0
  37. package/src/server/handlers/list-tools.ts +22 -5
  38. package/src/server/handlers/tools/brain.ts +85 -0
  39. package/src/server/handlers/tools/init-project.ts +47 -0
  40. package/src/server/handlers/tools/schemas.ts +12 -0
  41. package/src/server/http-api.ts +188 -0
  42. package/src/tools/registry.ts +9 -0
  43. package/src/tools/schemas.ts +33 -1
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Brain Response Filter
3
+ * Phase 16: Filters noise, deduplicates, ranks, and synthesizes results
4
+ * from the unified brain() tool
5
+ */
6
+
7
+ export interface FilterableResult {
8
+ content: string
9
+ score: number
10
+ source: string
11
+ metadata?: Record<string, unknown>
12
+ }
13
+
14
+ export interface FilteredResult {
15
+ content: string
16
+ score: number
17
+ source: string
18
+ relevanceNote: string
19
+ metadata?: Record<string, unknown>
20
+ }
21
+
22
+ export interface BrainResponse {
23
+ action: 'retrieved' | 'stored' | 'analyzed' | 'none'
24
+ summary: string
25
+ content: string
26
+ relevantItems: number
27
+ }
28
+
29
+ export interface TierResults {
30
+ label: string
31
+ results: FilterableResult[]
32
+ }
33
+
34
+ export class ResponseFilter {
35
+ // Infrastructure noise terms — strip from non-claude-brain projects
36
+ private readonly INFRA_NOISE = [
37
+ 'chromadb', 'chroma', 'minisearch', 'compromise', 'better-sqlite3',
38
+ 'pino', 'hono', 'bun:test', 'zod', 'mcp-server', 'claude-brain',
39
+ 'model-context-protocol', 'embedding-service', 'vector-database',
40
+ 'mcp tool', 'tool handler', 'phase 12', 'phase 13', 'phase 14', 'phase 15',
41
+ 'semantic cache', 'precompute engine', 'knowledge graph builder'
42
+ ]
43
+
44
+ /**
45
+ * Filter a set of results to remove noise and improve relevance
46
+ */
47
+ filter(results: FilterableResult[], query: string, project?: string): FilteredResult[] {
48
+ let filtered = results
49
+
50
+ // 1. Remove infrastructure noise (if project !== 'claude-brain')
51
+ if (project !== 'claude-brain') {
52
+ filtered = filtered.filter(r => !this.isInfrastructureNoise(r.content))
53
+ }
54
+
55
+ // 2. Remove near-duplicates (>85% word overlap, keep higher score)
56
+ filtered = this.deduplicateResults(filtered)
57
+
58
+ // 3. Apply dynamic threshold (at least 70% of median similarity)
59
+ filtered = this.applyDynamicThreshold(filtered)
60
+
61
+ // 4. Sort by score descending
62
+ filtered.sort((a, b) => b.score - a.score)
63
+
64
+ // 5. Limit to 5 results
65
+ filtered = filtered.slice(0, 5)
66
+
67
+ // 6. Add one-line relevance explanation per result
68
+ return filtered.map(r => ({
69
+ content: r.content,
70
+ score: r.score,
71
+ source: r.source,
72
+ relevanceNote: this.generateRelevanceNote(r, query),
73
+ metadata: r.metadata
74
+ }))
75
+ }
76
+
77
+ /**
78
+ * Synthesize results from multiple tiers into a unified BrainResponse
79
+ */
80
+ synthesize(
81
+ tiers: TierResults[],
82
+ message: string,
83
+ project?: string,
84
+ action: BrainResponse['action'] = 'retrieved'
85
+ ): BrainResponse {
86
+ // Combine all results from all tiers
87
+ const allResults: FilterableResult[] = []
88
+ for (const tier of tiers) {
89
+ allResults.push(...tier.results)
90
+ }
91
+
92
+ if (allResults.length === 0) {
93
+ return {
94
+ action: 'none',
95
+ summary: 'No relevant information found',
96
+ content: `No results found for: "${message.slice(0, 100)}"`,
97
+ relevantItems: 0
98
+ }
99
+ }
100
+
101
+ // Filter the combined results
102
+ const filtered = this.filter(allResults, message, project)
103
+
104
+ if (filtered.length === 0) {
105
+ return {
106
+ action: 'none',
107
+ summary: 'Results filtered out as noise or irrelevant',
108
+ content: `No relevant results after filtering for: "${message.slice(0, 100)}"`,
109
+ relevantItems: 0
110
+ }
111
+ }
112
+
113
+ // Format filtered results
114
+ const contentParts: string[] = []
115
+ for (const result of filtered) {
116
+ const scoreStr = result.score > 0 ? ` [${Math.round(result.score * 100)}%]` : ''
117
+ contentParts.push(`**${result.source}**${scoreStr}\n${result.content}`)
118
+ if (result.relevanceNote) {
119
+ contentParts.push(`_${result.relevanceNote}_`)
120
+ }
121
+ contentParts.push('')
122
+ }
123
+
124
+ const summary = filtered.length === 1
125
+ ? `Found 1 relevant result`
126
+ : `Found ${filtered.length} relevant results`
127
+
128
+ return {
129
+ action,
130
+ summary,
131
+ content: contentParts.join('\n'),
132
+ relevantItems: filtered.length
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Check if content is infrastructure/internal noise
138
+ */
139
+ private isInfrastructureNoise(content: string): boolean {
140
+ const lower = content.toLowerCase()
141
+ let noiseHits = 0
142
+
143
+ for (const term of this.INFRA_NOISE) {
144
+ if (lower.includes(term)) {
145
+ noiseHits++
146
+ }
147
+ }
148
+
149
+ // If more than 2 infrastructure terms appear, it's likely noise
150
+ return noiseHits >= 2
151
+ }
152
+
153
+ /**
154
+ * Remove near-duplicate results (>85% word overlap)
155
+ */
156
+ private deduplicateResults(results: FilterableResult[]): FilterableResult[] {
157
+ const kept: FilterableResult[] = []
158
+
159
+ for (const result of results) {
160
+ const isDuplicate = kept.some(existing => {
161
+ const overlap = this.calculateWordOverlap(existing.content, result.content)
162
+ return overlap > 0.85
163
+ })
164
+
165
+ if (!isDuplicate) {
166
+ kept.push(result)
167
+ }
168
+ }
169
+
170
+ return kept
171
+ }
172
+
173
+ /**
174
+ * Apply dynamic threshold — results must be at least 70% of median score
175
+ */
176
+ private applyDynamicThreshold(results: FilterableResult[]): FilterableResult[] {
177
+ if (results.length <= 1) return results
178
+
179
+ // Calculate median score
180
+ const scores = results.map(r => r.score).sort((a, b) => a - b)
181
+ const mid = Math.floor(scores.length / 2)
182
+ const left = scores[mid - 1] ?? 0
183
+ const right = scores[mid] ?? 0
184
+ const median = scores.length % 2 === 0
185
+ ? (left + right) / 2
186
+ : right
187
+
188
+ const threshold = median * 0.7
189
+
190
+ return results.filter(r => r.score >= threshold)
191
+ }
192
+
193
+ /**
194
+ * Calculate word overlap between two strings (0-1)
195
+ */
196
+ private calculateWordOverlap(a: string, b: string): number {
197
+ const wordsA = new Set(a.toLowerCase().split(/\s+/).filter(w => w.length > 2))
198
+ const wordsB = new Set(b.toLowerCase().split(/\s+/).filter(w => w.length > 2))
199
+
200
+ if (wordsA.size === 0 || wordsB.size === 0) return 0
201
+
202
+ let intersection = 0
203
+ for (const word of wordsA) {
204
+ if (wordsB.has(word)) intersection++
205
+ }
206
+
207
+ const smaller = Math.min(wordsA.size, wordsB.size)
208
+ return intersection / smaller
209
+ }
210
+
211
+ /**
212
+ * Generate a one-line relevance explanation
213
+ */
214
+ private generateRelevanceNote(result: FilterableResult, _query: string): string {
215
+ const score = Math.round(result.score * 100)
216
+ if (score >= 90) return `Highly relevant match (${score}%)`
217
+ if (score >= 70) return `Good match (${score}%)`
218
+ if (score >= 50) return `Partial match (${score}%)`
219
+ return `Low relevance match (${score}%)`
220
+ }
221
+ }