claude-brain 0.5.0 → 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.
- package/VERSION +1 -1
- package/assets/CLAUDE-unified.md +11 -0
- package/package.json +2 -1
- package/packs/backend/node.json +173 -0
- package/packs/core/javascript.json +176 -0
- package/packs/core/typescript.json +222 -0
- package/packs/frontend/react.json +254 -0
- package/packs/meta/testing.json +172 -0
- package/src/cli/bin.ts +14 -0
- package/src/cli/commands/chroma.ts +53 -17
- package/src/cli/commands/hooks.ts +214 -0
- package/src/cli/commands/pack.ts +197 -0
- package/src/cli/commands/serve.ts +34 -0
- package/src/config/defaults.ts +1 -1
- package/src/config/schema.ts +85 -2
- package/src/hooks/brain-hook.ts +110 -0
- package/src/hooks/capture.ts +161 -0
- package/src/hooks/deduplicator.ts +72 -0
- package/src/hooks/index.ts +19 -0
- package/src/hooks/installer.ts +181 -0
- package/src/hooks/passive-classifier.ts +366 -0
- package/src/hooks/queue.ts +122 -0
- package/src/hooks/session-tracker.ts +199 -0
- package/src/hooks/types.ts +47 -0
- package/src/memory/chroma/client.ts +1 -1
- package/src/memory/chroma/index.ts +1 -1
- package/src/memory/chroma/store.ts +29 -9
- package/src/memory/index.ts +1 -0
- package/src/memory/store.ts +1 -0
- package/src/packs/index.ts +9 -0
- package/src/packs/loader.ts +134 -0
- package/src/packs/manager.ts +204 -0
- package/src/packs/ranker.ts +78 -0
- package/src/packs/types.ts +81 -0
- package/src/routing/entity-extractor.ts +410 -0
- package/src/routing/intent-classifier.ts +229 -0
- package/src/routing/response-filter.ts +221 -0
- package/src/routing/router.ts +671 -0
- package/src/server/handlers/call-tool.ts +7 -0
- package/src/server/handlers/list-tools.ts +22 -5
- package/src/server/handlers/tools/brain.ts +85 -0
- package/src/server/handlers/tools/init-project.ts +47 -0
- package/src/server/handlers/tools/schemas.ts +12 -0
- package/src/server/http-api.ts +188 -0
- package/src/tools/registry.ts +9 -0
- package/src/tools/schemas.ts +33 -1
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brain Router
|
|
3
|
+
* Phase 16: Core orchestrator for the unified brain() tool
|
|
4
|
+
*
|
|
5
|
+
* Routes classified intents to internal service calls and
|
|
6
|
+
* returns unified BrainResponse objects.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Logger } from 'pino'
|
|
10
|
+
import { IntentClassifier, type ClassificationResult } from './intent-classifier'
|
|
11
|
+
import { BrainEntityExtractor, type BrainExtractedEntities } from './entity-extractor'
|
|
12
|
+
import { ResponseFilter, type BrainResponse, type TierResults } from './response-filter'
|
|
13
|
+
import {
|
|
14
|
+
getMemoryService,
|
|
15
|
+
getVaultService,
|
|
16
|
+
getContextService,
|
|
17
|
+
getPhase12Service,
|
|
18
|
+
getKnowledgeGraphService,
|
|
19
|
+
isServicesInitialized
|
|
20
|
+
} from '@/server/services'
|
|
21
|
+
|
|
22
|
+
export interface BrainInput {
|
|
23
|
+
message: string
|
|
24
|
+
project?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class BrainRouter {
|
|
28
|
+
private classifier: IntentClassifier
|
|
29
|
+
private entityExtractor: BrainEntityExtractor
|
|
30
|
+
private responseFilter: ResponseFilter
|
|
31
|
+
private logger: Logger
|
|
32
|
+
|
|
33
|
+
constructor(logger: Logger) {
|
|
34
|
+
this.classifier = new IntentClassifier()
|
|
35
|
+
this.entityExtractor = new BrainEntityExtractor()
|
|
36
|
+
this.responseFilter = new ResponseFilter()
|
|
37
|
+
this.logger = logger.child({ component: 'brain-router' })
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async route(input: BrainInput): Promise<BrainResponse> {
|
|
41
|
+
const { message, project: inputProject } = input
|
|
42
|
+
|
|
43
|
+
// Classify intent
|
|
44
|
+
const classification = this.classifier.classify(message)
|
|
45
|
+
this.logger.debug({ intent: classification.primary, confidence: classification.confidence }, 'Intent classified')
|
|
46
|
+
|
|
47
|
+
// Extract entities
|
|
48
|
+
const entities = await this.entityExtractor.extract(message, inputProject)
|
|
49
|
+
const project = entities.project || inputProject
|
|
50
|
+
this.logger.debug({ project, technologies: entities.technologies }, 'Entities extracted')
|
|
51
|
+
|
|
52
|
+
// Route to handler
|
|
53
|
+
try {
|
|
54
|
+
switch (classification.primary) {
|
|
55
|
+
case 'no_action':
|
|
56
|
+
return this.handleNoAction(message)
|
|
57
|
+
|
|
58
|
+
case 'session_start':
|
|
59
|
+
return this.handleSessionStart(message, project, entities)
|
|
60
|
+
|
|
61
|
+
case 'context_needed':
|
|
62
|
+
return this.handleContextNeeded(message, project, entities)
|
|
63
|
+
|
|
64
|
+
case 'decision_made':
|
|
65
|
+
return this.handleDecisionMade(message, project, entities)
|
|
66
|
+
|
|
67
|
+
case 'pattern_found':
|
|
68
|
+
return this.handlePatternFound(message, project, entities)
|
|
69
|
+
|
|
70
|
+
case 'mistake_learned':
|
|
71
|
+
return this.handleMistakeLearned(message, project, entities)
|
|
72
|
+
|
|
73
|
+
case 'progress_update':
|
|
74
|
+
return this.handleProgressUpdate(message, project, entities)
|
|
75
|
+
|
|
76
|
+
case 'question':
|
|
77
|
+
return this.handleQuestion(message, project, entities, classification)
|
|
78
|
+
|
|
79
|
+
case 'comparison':
|
|
80
|
+
return this.handleComparison(message, project, entities)
|
|
81
|
+
|
|
82
|
+
case 'exploration':
|
|
83
|
+
return this.handleExploration(message, project, entities)
|
|
84
|
+
|
|
85
|
+
default:
|
|
86
|
+
return this.handleContextNeeded(message, project, entities)
|
|
87
|
+
}
|
|
88
|
+
} catch (error) {
|
|
89
|
+
this.logger.error({ error, intent: classification.primary }, 'Router handler error')
|
|
90
|
+
return {
|
|
91
|
+
action: 'none',
|
|
92
|
+
summary: `Error processing request`,
|
|
93
|
+
content: `Failed to process: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
94
|
+
relevantItems: 0
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ===== Intent Handlers =====
|
|
100
|
+
|
|
101
|
+
private handleNoAction(_message: string): BrainResponse {
|
|
102
|
+
return {
|
|
103
|
+
action: 'none',
|
|
104
|
+
summary: 'No action needed',
|
|
105
|
+
content: '',
|
|
106
|
+
relevantItems: 0
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private async handleSessionStart(
|
|
111
|
+
message: string,
|
|
112
|
+
project: string | undefined,
|
|
113
|
+
entities: BrainExtractedEntities
|
|
114
|
+
): Promise<BrainResponse> {
|
|
115
|
+
if (!project) {
|
|
116
|
+
return {
|
|
117
|
+
action: 'none',
|
|
118
|
+
summary: 'No project detected',
|
|
119
|
+
content: 'Could not determine project. Please specify which project you are working on.',
|
|
120
|
+
relevantItems: 0
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!isServicesInitialized()) {
|
|
125
|
+
return this.servicesNotReady()
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const contextService = getContextService()
|
|
129
|
+
const phase12 = getPhase12Service()
|
|
130
|
+
|
|
131
|
+
// Get project context
|
|
132
|
+
const context = await contextService.getContext(project, {
|
|
133
|
+
includeMemories: false,
|
|
134
|
+
includeProgress: true,
|
|
135
|
+
includeStandards: true,
|
|
136
|
+
maxTokens: 6000,
|
|
137
|
+
relevanceThreshold: 0.5
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
const formattedContext = contextService.formatter.format(context)
|
|
141
|
+
|
|
142
|
+
// Process with Phase 12 for proactive recall
|
|
143
|
+
const phase12Result = await phase12.processMessage(
|
|
144
|
+
entities.topic || message,
|
|
145
|
+
project
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
// Build response
|
|
149
|
+
const parts: string[] = [formattedContext]
|
|
150
|
+
|
|
151
|
+
if (phase12Result.recalledMemories?.memories.length) {
|
|
152
|
+
parts.push('\n---\n## Relevant Past Decisions\n')
|
|
153
|
+
for (const mem of phase12Result.recalledMemories.memories) {
|
|
154
|
+
const similarity = Math.round(mem.similarity * 100)
|
|
155
|
+
parts.push(`**[${similarity}%]** ${mem.decision?.decision || mem.memory?.content?.slice(0, 100) || ''}`)
|
|
156
|
+
if (mem.decision?.reasoning) {
|
|
157
|
+
parts.push(` _${mem.decision.reasoning}_`)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// If Phase 12 found nothing, do a direct search fallback
|
|
163
|
+
if (!phase12Result.recalledMemories?.memories.length) {
|
|
164
|
+
try {
|
|
165
|
+
const memory = getMemoryService()
|
|
166
|
+
const directResults = await memory.searchRaw(entities.topic || message, {
|
|
167
|
+
project,
|
|
168
|
+
limit: 5,
|
|
169
|
+
minSimilarity: 0.3
|
|
170
|
+
})
|
|
171
|
+
if (directResults.length > 0) {
|
|
172
|
+
parts.push('\n---\n## Related Memories\n')
|
|
173
|
+
for (const r of directResults) {
|
|
174
|
+
const similarity = Math.round((r.similarity || 0) * 100)
|
|
175
|
+
parts.push(`**[${similarity}%]** ${r.decision?.decision || r.content?.slice(0, 100) || ''}`)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
} catch {
|
|
179
|
+
// Direct search failed, continue without
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const totalRecalled = phase12Result.recalledMemories?.memories.length || 0
|
|
184
|
+
return {
|
|
185
|
+
action: 'retrieved',
|
|
186
|
+
summary: `Session context for ${project}${totalRecalled ? ` (${totalRecalled} memories)` : ''}`,
|
|
187
|
+
content: parts.join('\n'),
|
|
188
|
+
relevantItems: totalRecalled
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private async handleContextNeeded(
|
|
193
|
+
message: string,
|
|
194
|
+
project: string | undefined,
|
|
195
|
+
entities: BrainExtractedEntities
|
|
196
|
+
): Promise<BrainResponse> {
|
|
197
|
+
if (!isServicesInitialized()) {
|
|
198
|
+
return this.servicesNotReady()
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const memory = getMemoryService()
|
|
202
|
+
const query = entities.topic || message
|
|
203
|
+
|
|
204
|
+
// Search for relevant memories
|
|
205
|
+
const tiers: TierResults[] = []
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
const rawResults = await memory.searchRaw(query, {
|
|
209
|
+
project,
|
|
210
|
+
limit: 5,
|
|
211
|
+
minSimilarity: 0.3
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
tiers.push({
|
|
215
|
+
label: 'Memories',
|
|
216
|
+
results: rawResults.map(r => ({
|
|
217
|
+
content: r.decision?.decision
|
|
218
|
+
? `**${r.decision.decision}**\n${r.decision.reasoning || ''}\n_Context: ${r.decision.context || ''}_`
|
|
219
|
+
: r.content?.slice(0, 300) || '',
|
|
220
|
+
score: r.similarity || 0,
|
|
221
|
+
source: 'Past Decision',
|
|
222
|
+
metadata: r.metadata
|
|
223
|
+
}))
|
|
224
|
+
})
|
|
225
|
+
} catch {
|
|
226
|
+
// Search failed, continue
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Also search patterns if project is available
|
|
230
|
+
try {
|
|
231
|
+
const patternResults = await memory.searchPatterns(query, {
|
|
232
|
+
project,
|
|
233
|
+
limit: 3,
|
|
234
|
+
minSimilarity: 0.3
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
if (patternResults.length > 0) {
|
|
238
|
+
tiers.push({
|
|
239
|
+
label: 'Patterns',
|
|
240
|
+
results: patternResults.map(p => ({
|
|
241
|
+
content: p.metadata?.description || p.content || '',
|
|
242
|
+
score: p.similarity || 0,
|
|
243
|
+
source: `Pattern (${p.metadata?.pattern_type || 'general'})`,
|
|
244
|
+
metadata: p.metadata
|
|
245
|
+
}))
|
|
246
|
+
})
|
|
247
|
+
}
|
|
248
|
+
} catch {
|
|
249
|
+
// Pattern search not available
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return this.responseFilter.synthesize(tiers, message, project)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private async handleDecisionMade(
|
|
256
|
+
message: string,
|
|
257
|
+
project: string | undefined,
|
|
258
|
+
entities: BrainExtractedEntities
|
|
259
|
+
): Promise<BrainResponse> {
|
|
260
|
+
if (!project) {
|
|
261
|
+
return {
|
|
262
|
+
action: 'none',
|
|
263
|
+
summary: 'Cannot store decision without a project',
|
|
264
|
+
content: 'Please specify which project this decision relates to.',
|
|
265
|
+
relevantItems: 0
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (!isServicesInitialized()) {
|
|
270
|
+
return this.servicesNotReady()
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const memory = getMemoryService()
|
|
274
|
+
const vault = getVaultService()
|
|
275
|
+
|
|
276
|
+
// Extract decision components (use entities or fall back to raw message)
|
|
277
|
+
const decision = entities.decision || message
|
|
278
|
+
const reasoning = entities.reasoning || ''
|
|
279
|
+
const context = entities.topic || message.slice(0, 200)
|
|
280
|
+
const alternatives = entities.alternatives
|
|
281
|
+
|
|
282
|
+
const decisionId = await memory.rememberDecision(
|
|
283
|
+
project,
|
|
284
|
+
context,
|
|
285
|
+
decision,
|
|
286
|
+
reasoning,
|
|
287
|
+
{
|
|
288
|
+
alternatives,
|
|
289
|
+
tags: entities.technologies.length > 0 ? entities.technologies : ['auto-detected']
|
|
290
|
+
}
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
// Also write to vault
|
|
294
|
+
try {
|
|
295
|
+
const projectPaths = vault.getProjectPaths(project)
|
|
296
|
+
const date = new Date().toISOString().split('T')[0]
|
|
297
|
+
const entry = `### Decision: ${decision.slice(0, 100)}\n\n**Date:** ${date}\n**Context:** ${context}\n**Decision:** ${decision}\n**Reasoning:** ${reasoning}\n${alternatives ? `**Alternatives:** ${alternatives}\n` : ''}\n---\n\n`
|
|
298
|
+
await vault.writer.appendContent(projectPaths.decisions, entry, '\n')
|
|
299
|
+
} catch {
|
|
300
|
+
// Vault write failed — memory storage still succeeded
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
action: 'stored',
|
|
305
|
+
summary: `Stored decision: ${decision.slice(0, 60)}`,
|
|
306
|
+
content: `Decision stored (ID: ${decisionId})\n\n**Project:** ${project}\n**Decision:** ${decision}\n**Reasoning:** ${reasoning}`,
|
|
307
|
+
relevantItems: 1
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
private async handlePatternFound(
|
|
312
|
+
message: string,
|
|
313
|
+
project: string | undefined,
|
|
314
|
+
entities: BrainExtractedEntities
|
|
315
|
+
): Promise<BrainResponse> {
|
|
316
|
+
if (!project) {
|
|
317
|
+
return {
|
|
318
|
+
action: 'none',
|
|
319
|
+
summary: 'Cannot store pattern without a project',
|
|
320
|
+
content: 'Please specify which project this pattern relates to.',
|
|
321
|
+
relevantItems: 0
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (!isServicesInitialized()) {
|
|
326
|
+
return this.servicesNotReady()
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const memory = getMemoryService()
|
|
330
|
+
const patternType = entities.patternType || 'solution'
|
|
331
|
+
const description = entities.topic || message
|
|
332
|
+
|
|
333
|
+
const patternId = await memory.storePattern({
|
|
334
|
+
project,
|
|
335
|
+
pattern_type: patternType,
|
|
336
|
+
description,
|
|
337
|
+
confidence: 0.8,
|
|
338
|
+
context: message.slice(0, 300)
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
action: 'stored',
|
|
343
|
+
summary: `Stored ${patternType}: ${description.slice(0, 60)}`,
|
|
344
|
+
content: `Pattern stored (ID: ${patternId})\n\n**Type:** ${patternType}\n**Project:** ${project}\n**Description:** ${description}`,
|
|
345
|
+
relevantItems: 1
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
private async handleMistakeLearned(
|
|
350
|
+
message: string,
|
|
351
|
+
project: string | undefined,
|
|
352
|
+
entities: BrainExtractedEntities
|
|
353
|
+
): Promise<BrainResponse> {
|
|
354
|
+
if (!project) {
|
|
355
|
+
return {
|
|
356
|
+
action: 'none',
|
|
357
|
+
summary: 'Cannot store correction without a project',
|
|
358
|
+
content: 'Please specify which project this lesson relates to.',
|
|
359
|
+
relevantItems: 0
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (!isServicesInitialized()) {
|
|
364
|
+
return this.servicesNotReady()
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const memory = getMemoryService()
|
|
368
|
+
|
|
369
|
+
const original = entities.original || message
|
|
370
|
+
const correction = entities.correction || ''
|
|
371
|
+
const reasoning = entities.reasoning || 'Lesson learned from experience'
|
|
372
|
+
|
|
373
|
+
const correctionId = await memory.storeCorrection({
|
|
374
|
+
project,
|
|
375
|
+
original,
|
|
376
|
+
correction: correction || message,
|
|
377
|
+
reasoning,
|
|
378
|
+
context: entities.topic || '',
|
|
379
|
+
confidence: 0.9
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
action: 'stored',
|
|
384
|
+
summary: `Stored correction: ${original.slice(0, 60)}`,
|
|
385
|
+
content: `Correction stored (ID: ${correctionId})\n\n**Project:** ${project}\n**Original:** ${original}\n**Correction:** ${correction || '(see original message)'}`,
|
|
386
|
+
relevantItems: 1
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
private async handleProgressUpdate(
|
|
391
|
+
message: string,
|
|
392
|
+
project: string | undefined,
|
|
393
|
+
entities: BrainExtractedEntities
|
|
394
|
+
): Promise<BrainResponse> {
|
|
395
|
+
if (!project) {
|
|
396
|
+
return {
|
|
397
|
+
action: 'none',
|
|
398
|
+
summary: 'Cannot update progress without a project',
|
|
399
|
+
content: 'Please specify which project to update progress for.',
|
|
400
|
+
relevantItems: 0
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (!isServicesInitialized()) {
|
|
405
|
+
return this.servicesNotReady()
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const contextService = getContextService()
|
|
409
|
+
|
|
410
|
+
const completedTask = entities.completedTask || message
|
|
411
|
+
const nextSteps = entities.nextSteps || 'Continue development'
|
|
412
|
+
|
|
413
|
+
try {
|
|
414
|
+
await contextService.progress.addCompletedTask(project, {
|
|
415
|
+
id: this.generateTaskId(completedTask),
|
|
416
|
+
title: completedTask,
|
|
417
|
+
status: 'done',
|
|
418
|
+
completedAt: new Date()
|
|
419
|
+
})
|
|
420
|
+
} catch {
|
|
421
|
+
// Progress update failed — still report what we found
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return {
|
|
425
|
+
action: 'stored',
|
|
426
|
+
summary: `Progress: ${completedTask.slice(0, 60)}`,
|
|
427
|
+
content: `Progress updated for ${project}\n\n**Completed:** ${completedTask}\n**Next:** ${nextSteps}`,
|
|
428
|
+
relevantItems: 1
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
private async handleQuestion(
|
|
433
|
+
message: string,
|
|
434
|
+
project: string | undefined,
|
|
435
|
+
entities: BrainExtractedEntities,
|
|
436
|
+
classification: ClassificationResult
|
|
437
|
+
): Promise<BrainResponse> {
|
|
438
|
+
if (!isServicesInitialized()) {
|
|
439
|
+
return this.servicesNotReady()
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const memory = getMemoryService()
|
|
443
|
+
const query = entities.topic || message
|
|
444
|
+
const tiers: TierResults[] = []
|
|
445
|
+
|
|
446
|
+
// Parallel search: raw memories + patterns + corrections
|
|
447
|
+
const [rawResults, patternResults, correctionResults] = await Promise.all([
|
|
448
|
+
memory.searchRaw(query, {
|
|
449
|
+
project,
|
|
450
|
+
limit: 5,
|
|
451
|
+
minSimilarity: 0.3
|
|
452
|
+
}).catch(() => [] as any[]),
|
|
453
|
+
memory.searchPatterns(query, {
|
|
454
|
+
project,
|
|
455
|
+
limit: 3,
|
|
456
|
+
minSimilarity: 0.3
|
|
457
|
+
}).catch(() => [] as any[]),
|
|
458
|
+
memory.searchCorrections(query, {
|
|
459
|
+
project,
|
|
460
|
+
limit: 3,
|
|
461
|
+
minSimilarity: 0.3
|
|
462
|
+
}).catch(() => [] as any[])
|
|
463
|
+
])
|
|
464
|
+
|
|
465
|
+
if (rawResults.length > 0) {
|
|
466
|
+
tiers.push({
|
|
467
|
+
label: 'Memories',
|
|
468
|
+
results: rawResults.map((r: any) => ({
|
|
469
|
+
content: r.decision?.decision
|
|
470
|
+
? `**${r.decision.decision}**\n${r.decision.reasoning || ''}`
|
|
471
|
+
: r.content?.slice(0, 300) || '',
|
|
472
|
+
score: r.similarity || 0,
|
|
473
|
+
source: 'Past Decision',
|
|
474
|
+
metadata: r.metadata
|
|
475
|
+
}))
|
|
476
|
+
})
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (patternResults.length > 0) {
|
|
480
|
+
tiers.push({
|
|
481
|
+
label: 'Patterns',
|
|
482
|
+
results: patternResults.map((p: any) => ({
|
|
483
|
+
content: p.metadata?.description || p.content || '',
|
|
484
|
+
score: p.similarity || 0,
|
|
485
|
+
source: `Pattern (${p.metadata?.pattern_type || 'general'})`,
|
|
486
|
+
metadata: p.metadata
|
|
487
|
+
}))
|
|
488
|
+
})
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (correctionResults.length > 0) {
|
|
492
|
+
tiers.push({
|
|
493
|
+
label: 'Corrections',
|
|
494
|
+
results: correctionResults.map((c: any) => ({
|
|
495
|
+
content: `Original: ${c.metadata?.original || ''}\nFix: ${c.metadata?.correction || c.content || ''}`,
|
|
496
|
+
score: c.similarity || 0,
|
|
497
|
+
source: 'Lesson Learned',
|
|
498
|
+
metadata: c.metadata
|
|
499
|
+
}))
|
|
500
|
+
})
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// If secondary intent is exploration, also try graph search
|
|
504
|
+
if (classification.secondary.includes('exploration')) {
|
|
505
|
+
await this.addExplorationResults(query, project, tiers)
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return this.responseFilter.synthesize(tiers, message, project)
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
private async handleComparison(
|
|
512
|
+
message: string,
|
|
513
|
+
project: string | undefined,
|
|
514
|
+
entities: BrainExtractedEntities
|
|
515
|
+
): Promise<BrainResponse> {
|
|
516
|
+
if (!isServicesInitialized()) {
|
|
517
|
+
return this.servicesNotReady()
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const memory = getMemoryService()
|
|
521
|
+
const query = entities.topic || message
|
|
522
|
+
const tiers: TierResults[] = []
|
|
523
|
+
|
|
524
|
+
// Search for related decisions
|
|
525
|
+
try {
|
|
526
|
+
const rawResults = await memory.searchRaw(query, {
|
|
527
|
+
project,
|
|
528
|
+
limit: 5,
|
|
529
|
+
minSimilarity: 0.2
|
|
530
|
+
})
|
|
531
|
+
|
|
532
|
+
if (rawResults.length > 0) {
|
|
533
|
+
tiers.push({
|
|
534
|
+
label: 'Related Decisions',
|
|
535
|
+
results: rawResults.map((r: any) => ({
|
|
536
|
+
content: r.decision?.decision
|
|
537
|
+
? `**${r.decision.decision}**\n${r.decision.reasoning || ''}`
|
|
538
|
+
: r.content?.slice(0, 300) || '',
|
|
539
|
+
score: r.similarity || 0,
|
|
540
|
+
source: 'Past Decision'
|
|
541
|
+
}))
|
|
542
|
+
})
|
|
543
|
+
}
|
|
544
|
+
} catch {
|
|
545
|
+
// Search failed
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Try what-if analysis if knowledge graph is available
|
|
549
|
+
try {
|
|
550
|
+
const kgService = getKnowledgeGraphService()
|
|
551
|
+
if (kgService?.search) {
|
|
552
|
+
const graphResults = kgService.search.search({ query, limit: 5 })
|
|
553
|
+
if (graphResults?.nodes?.length) {
|
|
554
|
+
tiers.push({
|
|
555
|
+
label: 'Knowledge Graph',
|
|
556
|
+
results: graphResults.nodes.map((n: any) => ({
|
|
557
|
+
content: `**${n.label || n.name}** (${n.type})\n${n.metadata?.description || ''}`,
|
|
558
|
+
score: n.score || 0.5,
|
|
559
|
+
source: 'Knowledge Graph'
|
|
560
|
+
}))
|
|
561
|
+
})
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
} catch {
|
|
565
|
+
// Knowledge graph not available
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return this.responseFilter.synthesize(tiers, message, project, 'analyzed')
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
private async handleExploration(
|
|
572
|
+
message: string,
|
|
573
|
+
project: string | undefined,
|
|
574
|
+
entities: BrainExtractedEntities
|
|
575
|
+
): Promise<BrainResponse> {
|
|
576
|
+
if (!isServicesInitialized()) {
|
|
577
|
+
return this.servicesNotReady()
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const query = entities.topic || message
|
|
581
|
+
const tiers: TierResults[] = []
|
|
582
|
+
|
|
583
|
+
await this.addExplorationResults(query, project, tiers)
|
|
584
|
+
|
|
585
|
+
// Also do a basic memory search
|
|
586
|
+
try {
|
|
587
|
+
const memory = getMemoryService()
|
|
588
|
+
const rawResults = await memory.searchRaw(query, {
|
|
589
|
+
project,
|
|
590
|
+
limit: 5,
|
|
591
|
+
minSimilarity: 0.2
|
|
592
|
+
})
|
|
593
|
+
|
|
594
|
+
if (rawResults.length > 0) {
|
|
595
|
+
tiers.push({
|
|
596
|
+
label: 'Memories',
|
|
597
|
+
results: rawResults.map((r: any) => ({
|
|
598
|
+
content: r.decision?.decision
|
|
599
|
+
? `**${r.decision.decision}**\n${r.decision.reasoning || ''}`
|
|
600
|
+
: r.content?.slice(0, 300) || '',
|
|
601
|
+
score: r.similarity || 0,
|
|
602
|
+
source: 'Past Decision'
|
|
603
|
+
}))
|
|
604
|
+
})
|
|
605
|
+
}
|
|
606
|
+
} catch {
|
|
607
|
+
// Search failed
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return this.responseFilter.synthesize(tiers, message, project, 'analyzed')
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// ===== Helpers =====
|
|
614
|
+
|
|
615
|
+
private async addExplorationResults(
|
|
616
|
+
query: string,
|
|
617
|
+
_project: string | undefined,
|
|
618
|
+
tiers: TierResults[]
|
|
619
|
+
): Promise<void> {
|
|
620
|
+
try {
|
|
621
|
+
const kgService = getKnowledgeGraphService()
|
|
622
|
+
if (kgService?.search) {
|
|
623
|
+
const graphResults = kgService.search.search({ query, limit: 10 })
|
|
624
|
+
if (graphResults?.nodes?.length) {
|
|
625
|
+
tiers.push({
|
|
626
|
+
label: 'Knowledge Graph',
|
|
627
|
+
results: graphResults.nodes.map((n: any) => ({
|
|
628
|
+
content: `**${n.label || n.name}** (${n.type})${n.metadata?.description ? `\n${n.metadata.description}` : ''}`,
|
|
629
|
+
score: n.score || 0.5,
|
|
630
|
+
source: 'Knowledge Graph'
|
|
631
|
+
}))
|
|
632
|
+
})
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
} catch {
|
|
636
|
+
// Knowledge graph not available
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
private servicesNotReady(): BrainResponse {
|
|
641
|
+
return {
|
|
642
|
+
action: 'none',
|
|
643
|
+
summary: 'Services not initialized',
|
|
644
|
+
content: 'Claude Brain services are not ready. The server may still be starting up.',
|
|
645
|
+
relevantItems: 0
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
private generateTaskId(title: string): string {
|
|
650
|
+
return title
|
|
651
|
+
.toLowerCase()
|
|
652
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
653
|
+
.replace(/^-+|-+$/g, '')
|
|
654
|
+
.slice(0, 50)
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Lazy singleton
|
|
659
|
+
let routerInstance: BrainRouter | null = null
|
|
660
|
+
|
|
661
|
+
export function getBrainRouter(logger: Logger): BrainRouter {
|
|
662
|
+
if (!routerInstance) {
|
|
663
|
+
routerInstance = new BrainRouter(logger)
|
|
664
|
+
}
|
|
665
|
+
return routerInstance
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/** Reset singleton for testing */
|
|
669
|
+
export function _resetBrainRouterForTesting(): void {
|
|
670
|
+
routerInstance = null
|
|
671
|
+
}
|
|
@@ -47,6 +47,9 @@ import { handleWhatIfAnalysis } from './tools/what-if-analysis'
|
|
|
47
47
|
import { handleGetRecommendations } from './tools/get-recommendations'
|
|
48
48
|
import { handleFindCrossProjectPatterns } from './tools/find-cross-project-patterns'
|
|
49
49
|
|
|
50
|
+
// Phase 16 Unified Brain Tool
|
|
51
|
+
import { handleBrain } from './tools/brain'
|
|
52
|
+
|
|
50
53
|
/**
|
|
51
54
|
* Handle tools/call request
|
|
52
55
|
* Validates that tool exists and routes to the appropriate handler
|
|
@@ -164,6 +167,10 @@ export async function handleCallTool(
|
|
|
164
167
|
case 'find_cross_project_patterns':
|
|
165
168
|
return await handleFindCrossProjectPatterns(args, logger)
|
|
166
169
|
|
|
170
|
+
// Phase 16 Unified Brain Tool
|
|
171
|
+
case 'brain':
|
|
172
|
+
return await handleBrain(args, logger)
|
|
173
|
+
|
|
167
174
|
default:
|
|
168
175
|
// This should never happen if validateToolExists works correctly
|
|
169
176
|
throw new McpError(
|