claude-brain 0.9.3 → 0.10.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/package.json +1 -1
- package/src/config/defaults.ts +1 -1
- package/src/config/schema.ts +1 -1
- package/src/memory/chroma/store.ts +6 -1
- package/src/memory/index.ts +23 -12
- package/src/routing/intent-classifier.ts +94 -6
- package/src/routing/response-filter.ts +50 -17
- package/src/routing/router.ts +445 -221
- package/src/routing/search-engine.ts +455 -0
- package/src/routing/types.ts +84 -0
- package/src/server/handlers/call-tool.ts +4 -49
- package/src/server/handlers/tools/index.ts +5 -7
- package/src/server/services.ts +28 -0
- package/src/tools/schemas.ts +6 -328
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.10.0
|
package/package.json
CHANGED
package/src/config/defaults.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { PartialConfig } from './schema'
|
|
|
3
3
|
/** Default configuration values for Claude Brain */
|
|
4
4
|
export const defaultConfig: PartialConfig = {
|
|
5
5
|
serverName: 'claude-brain',
|
|
6
|
-
serverVersion: '0.
|
|
6
|
+
serverVersion: '0.10.0',
|
|
7
7
|
logLevel: 'info',
|
|
8
8
|
logFilePath: './logs/claude-brain.log',
|
|
9
9
|
dbPath: './data/memory.db',
|
package/src/config/schema.ts
CHANGED
|
@@ -270,7 +270,7 @@ export const ConfigSchema = z.object({
|
|
|
270
270
|
serverName: z.string().default('claude-brain'),
|
|
271
271
|
|
|
272
272
|
/** Server version in semver format */
|
|
273
|
-
serverVersion: z.string().regex(/^\d+\.\d+\.\d+$/, 'Version must be semver format').default('0.
|
|
273
|
+
serverVersion: z.string().regex(/^\d+\.\d+\.\d+$/, 'Version must be semver format').default('0.10.0'),
|
|
274
274
|
|
|
275
275
|
/** Logging level */
|
|
276
276
|
logLevel: LogLevelSchema.default('info'),
|
|
@@ -179,7 +179,12 @@ export class ChromaMemoryStore {
|
|
|
179
179
|
confidence: 1.0,
|
|
180
180
|
created_at: now,
|
|
181
181
|
updated_at: now,
|
|
182
|
-
decision_id: id
|
|
182
|
+
decision_id: id,
|
|
183
|
+
// Phase 19: Include decision fields so memories collection results
|
|
184
|
+
// can surface decision content without cross-collection lookup
|
|
185
|
+
decision: input.decision,
|
|
186
|
+
reasoning: input.reasoning,
|
|
187
|
+
context: input.context
|
|
183
188
|
}
|
|
184
189
|
|
|
185
190
|
await memoriesCollection.add({
|
package/src/memory/index.ts
CHANGED
|
@@ -244,27 +244,38 @@ export class MemoryManager {
|
|
|
244
244
|
minSimilarity: options?.minSimilarity || 0.5
|
|
245
245
|
})
|
|
246
246
|
// Transform ChromaDB results to match MemorySearchResult structure
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
content: typeof r.content === 'string' ? r.content : JSON.stringify(r.content),
|
|
252
|
-
createdAt: r.metadata.created_at ? new Date(r.metadata.created_at) : new Date(),
|
|
253
|
-
metadata: r.metadata
|
|
254
|
-
},
|
|
255
|
-
similarity: r.similarity,
|
|
256
|
-
decision: r.metadata.decision ? {
|
|
247
|
+
// Includes flat `content` field for direct access (Phase 19)
|
|
248
|
+
return chromaResults.map(r => {
|
|
249
|
+
const memoryContent = typeof r.content === 'string' ? r.content : JSON.stringify(r.content)
|
|
250
|
+
const decisionObj = r.metadata.decision ? {
|
|
257
251
|
id: r.id,
|
|
258
252
|
project: r.metadata.project || options?.project || 'unknown',
|
|
259
253
|
context: r.metadata.context || '',
|
|
260
|
-
decision: r.metadata.decision ||
|
|
254
|
+
decision: r.metadata.decision || memoryContent,
|
|
261
255
|
reasoning: r.metadata.reasoning || '',
|
|
262
256
|
alternatives: r.metadata.alternatives_considered || '',
|
|
263
257
|
tags: r.metadata.tags || [],
|
|
264
258
|
outcome: r.metadata.outcome,
|
|
265
259
|
createdAt: r.metadata.created_at ? new Date(r.metadata.created_at) : new Date()
|
|
266
260
|
} : undefined
|
|
267
|
-
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
// Flat fields for direct access (Phase 19)
|
|
264
|
+
id: r.id,
|
|
265
|
+
content: decisionObj ? decisionObj.decision : memoryContent,
|
|
266
|
+
// Nested fields for backward compatibility
|
|
267
|
+
memory: {
|
|
268
|
+
id: r.id,
|
|
269
|
+
project: r.metadata.project || options?.project || 'unknown',
|
|
270
|
+
content: memoryContent,
|
|
271
|
+
createdAt: r.metadata.created_at ? new Date(r.metadata.created_at) : new Date(),
|
|
272
|
+
metadata: r.metadata
|
|
273
|
+
},
|
|
274
|
+
similarity: r.similarity,
|
|
275
|
+
decision: decisionObj,
|
|
276
|
+
metadata: r.metadata
|
|
277
|
+
}
|
|
278
|
+
})
|
|
268
279
|
} else {
|
|
269
280
|
return await this.search.search(query, {
|
|
270
281
|
project: options?.project,
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Brain Intent Classifier
|
|
3
|
-
* Phase 16: Rule-based intent classification for the unified brain() tool
|
|
3
|
+
* Phase 16 + Phase 19: Rule-based intent classification for the unified brain() tool
|
|
4
4
|
*
|
|
5
5
|
* Priority-ordered — first confident match wins.
|
|
6
6
|
* No LLM calls, pure pattern matching.
|
|
7
|
+
*
|
|
8
|
+
* Phase 19 changes:
|
|
9
|
+
* - B1: Question confidence raised (? → 0.95, question word → 0.90)
|
|
10
|
+
* - B2: session_start narrowed (must be at start + no progress indicators)
|
|
11
|
+
* - B3: Temporal signal detection added to secondary intents
|
|
7
12
|
*/
|
|
8
13
|
|
|
9
14
|
export type Intent =
|
|
@@ -102,13 +107,20 @@ const EXPLORATION_PHRASES = [
|
|
|
102
107
|
'decisions about', 'when did we', 'list episodes'
|
|
103
108
|
]
|
|
104
109
|
|
|
105
|
-
// Session start indicators
|
|
110
|
+
// Session start indicators — Phase 19: narrowed to require start-of-message
|
|
106
111
|
const SESSION_START_PHRASES = [
|
|
107
112
|
'starting work', 'beginning work', 'resuming',
|
|
108
113
|
'picking up', 'getting started', 'opening',
|
|
109
114
|
'starting on', 'working on', 'beginning on'
|
|
110
115
|
]
|
|
111
116
|
|
|
117
|
+
// Progress indicators that disqualify session_start
|
|
118
|
+
const PROGRESS_INDICATORS = [
|
|
119
|
+
'finished', 'completed', 'done', 'fixed', 'resolved',
|
|
120
|
+
'shipped', 'deployed', 'merged', 'released', 'just did',
|
|
121
|
+
'making progress', 'currently working'
|
|
122
|
+
]
|
|
123
|
+
|
|
112
124
|
// Question indicators
|
|
113
125
|
const QUESTION_WORDS = [
|
|
114
126
|
'what', 'how', 'when', 'why', 'where', 'which',
|
|
@@ -148,6 +160,21 @@ const DELETE_PHRASES = [
|
|
|
148
160
|
'remove the memory', 'clear that', 'drop that'
|
|
149
161
|
]
|
|
150
162
|
|
|
163
|
+
// Phase 19 B3: Temporal signal phrases
|
|
164
|
+
const TEMPORAL_PHRASES = [
|
|
165
|
+
'last week', 'last month', 'last year', 'yesterday', 'today',
|
|
166
|
+
'this week', 'this month', 'this year',
|
|
167
|
+
'since january', 'since february', 'since march', 'since april',
|
|
168
|
+
'since may', 'since june', 'since july', 'since august',
|
|
169
|
+
'since september', 'since october', 'since november', 'since december',
|
|
170
|
+
'in january', 'in february', 'in march', 'in april',
|
|
171
|
+
'in may', 'in june', 'in july', 'in august',
|
|
172
|
+
'in september', 'in october', 'in november', 'in december',
|
|
173
|
+
'ago', 'recently', 'before', 'after', 'during',
|
|
174
|
+
'last few days', 'past week', 'past month',
|
|
175
|
+
'earlier this', 'earlier today', 'over the past'
|
|
176
|
+
]
|
|
177
|
+
|
|
151
178
|
export class IntentClassifier {
|
|
152
179
|
/**
|
|
153
180
|
* Classify the intent of a message
|
|
@@ -156,6 +183,9 @@ export class IntentClassifier {
|
|
|
156
183
|
const lower = message.toLowerCase().trim()
|
|
157
184
|
const secondary: Intent[] = []
|
|
158
185
|
|
|
186
|
+
// Phase 19 B3: Detect temporal signals for secondary intent
|
|
187
|
+
const hasTemporal = this.hasTemporalSignal(lower)
|
|
188
|
+
|
|
159
189
|
// Check in priority order — first confident match wins
|
|
160
190
|
|
|
161
191
|
// 1. no_action: very short messages, greetings, acknowledgments
|
|
@@ -204,6 +234,7 @@ export class IntentClassifier {
|
|
|
204
234
|
// 9. comparison: vs, which is better, etc.
|
|
205
235
|
if (this.isComparison(lower)) {
|
|
206
236
|
if (this.isQuestion(lower, message)) secondary.push('question')
|
|
237
|
+
if (hasTemporal) secondary.push('exploration')
|
|
207
238
|
return { primary: 'comparison', confidence: 0.85, secondary }
|
|
208
239
|
}
|
|
209
240
|
|
|
@@ -212,7 +243,7 @@ export class IntentClassifier {
|
|
|
212
243
|
return { primary: 'pattern_found', confidence: 0.80, secondary }
|
|
213
244
|
}
|
|
214
245
|
|
|
215
|
-
// 11. session_start: starting/resuming work
|
|
246
|
+
// 11. session_start: starting/resuming work (Phase 19: narrowed check)
|
|
216
247
|
if (this.isSessionStart(lower)) {
|
|
217
248
|
secondary.push('context_needed')
|
|
218
249
|
return { primary: 'session_start', confidence: 0.90, secondary }
|
|
@@ -221,17 +252,24 @@ export class IntentClassifier {
|
|
|
221
252
|
// 12. exploration: timeline, trends, graph, history
|
|
222
253
|
if (this.isExploration(lower)) {
|
|
223
254
|
if (this.isQuestion(lower, message)) secondary.push('question')
|
|
255
|
+
if (hasTemporal) secondary.push('exploration')
|
|
224
256
|
return { primary: 'exploration', confidence: 0.75, secondary }
|
|
225
257
|
}
|
|
226
258
|
|
|
227
259
|
// 13. question: starts with question word or ends with ?
|
|
260
|
+
// Phase 19 B1: Higher confidence for questions
|
|
228
261
|
if (this.isQuestion(lower, message)) {
|
|
229
262
|
if (this.hasComparisonSignal(lower)) secondary.push('comparison')
|
|
230
263
|
if (this.hasExplorationSignal(lower)) secondary.push('exploration')
|
|
231
|
-
|
|
264
|
+
if (hasTemporal) secondary.push('exploration')
|
|
265
|
+
|
|
266
|
+
// Phase 19 B1: ? → 0.95, question word → 0.90
|
|
267
|
+
const confidence = message.trim().endsWith('?') ? 0.95 : 0.90
|
|
268
|
+
return { primary: 'question', confidence, secondary }
|
|
232
269
|
}
|
|
233
270
|
|
|
234
271
|
// 14. Default: context_needed
|
|
272
|
+
if (hasTemporal) secondary.push('exploration')
|
|
235
273
|
return { primary: 'context_needed', confidence: 0.60, secondary }
|
|
236
274
|
}
|
|
237
275
|
|
|
@@ -247,6 +285,8 @@ export class IntentClassifier {
|
|
|
247
285
|
if (original?.trim().endsWith('?')) return false
|
|
248
286
|
const firstWord = lower.split(/\s+/)[0] || ''
|
|
249
287
|
if (QUESTION_WORDS.includes(firstWord)) return false
|
|
288
|
+
// Phase 19 B4: Additional question guards
|
|
289
|
+
if (this.startsWithQuestionPattern(lower)) return false
|
|
250
290
|
return STORE_PHRASES.some(p => lower.includes(p))
|
|
251
291
|
}
|
|
252
292
|
|
|
@@ -255,6 +295,8 @@ export class IntentClassifier {
|
|
|
255
295
|
if (original?.trim().endsWith('?')) return false
|
|
256
296
|
const firstWord = lower.split(/\s+/)[0] || ''
|
|
257
297
|
if (QUESTION_WORDS.includes(firstWord)) return false
|
|
298
|
+
// Phase 19 B4: Additional question guards
|
|
299
|
+
if (this.startsWithQuestionPattern(lower)) return false
|
|
258
300
|
|
|
259
301
|
const hasDecision = DECISION_PHRASES.some(p => lower.includes(p))
|
|
260
302
|
if (!hasDecision) return false
|
|
@@ -289,8 +331,23 @@ export class IntentClassifier {
|
|
|
289
331
|
return hasPattern && lower.length > 50
|
|
290
332
|
}
|
|
291
333
|
|
|
334
|
+
/**
|
|
335
|
+
* Phase 19 B2: Narrowed session_start detection.
|
|
336
|
+
* Must start at beginning of message AND have no progress indicators.
|
|
337
|
+
* "working on X" in the middle of a sentence is not a session start.
|
|
338
|
+
*/
|
|
292
339
|
private isSessionStart(lower: string): boolean {
|
|
293
|
-
|
|
340
|
+
// Must NOT have progress indicators (e.g. "finished working on X" is progress, not session start)
|
|
341
|
+
if (PROGRESS_INDICATORS.some(p => lower.includes(p))) return false
|
|
342
|
+
|
|
343
|
+
// Phase 19 B2: Session phrases must match near the start of the message
|
|
344
|
+
for (const phrase of SESSION_START_PHRASES) {
|
|
345
|
+
const idx = lower.indexOf(phrase)
|
|
346
|
+
if (idx !== -1 && idx <= 5) {
|
|
347
|
+
return true
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return false
|
|
294
351
|
}
|
|
295
352
|
|
|
296
353
|
private isExploration(lower: string): boolean {
|
|
@@ -300,7 +357,10 @@ export class IntentClassifier {
|
|
|
300
357
|
private isQuestion(lower: string, original: string): boolean {
|
|
301
358
|
if (original.trim().endsWith('?')) return true
|
|
302
359
|
const firstWord = lower.split(/\s+/)[0] || ''
|
|
303
|
-
|
|
360
|
+
if (QUESTION_WORDS.includes(firstWord)) return true
|
|
361
|
+
// Phase 19 B4: "do I", "did we", "have we", "can we" patterns
|
|
362
|
+
if (this.startsWithQuestionPattern(lower)) return true
|
|
363
|
+
return false
|
|
304
364
|
}
|
|
305
365
|
|
|
306
366
|
private isUpdateMemory(lower: string): boolean {
|
|
@@ -327,4 +387,32 @@ export class IntentClassifier {
|
|
|
327
387
|
private hasSessionSignal(lower: string): boolean {
|
|
328
388
|
return SESSION_START_PHRASES.some(p => lower.includes(p))
|
|
329
389
|
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Phase 19 B3: Detect temporal signals in the message.
|
|
393
|
+
* Used to add 'temporal' context to secondary intents.
|
|
394
|
+
*/
|
|
395
|
+
hasTemporalSignal(lower: string): boolean {
|
|
396
|
+
return TEMPORAL_PHRASES.some(p => lower.includes(p))
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Phase 19 B4: Extended question guards.
|
|
401
|
+
* Catches "do I", "did we", "have we", "can we" etc. patterns
|
|
402
|
+
* that start with auxiliary verbs not in the QUESTION_WORDS list.
|
|
403
|
+
*/
|
|
404
|
+
private startsWithQuestionPattern(lower: string): boolean {
|
|
405
|
+
const questionStarters = [
|
|
406
|
+
'do i ', 'do we ', 'do you ',
|
|
407
|
+
'did i ', 'did we ', 'did you ',
|
|
408
|
+
'have i ', 'have we ', 'have you ',
|
|
409
|
+
'has it ', 'has the ',
|
|
410
|
+
'can i ', 'can we ', 'can you ',
|
|
411
|
+
'will i ', 'will we ', 'will you ',
|
|
412
|
+
'am i ', 'was i ', 'were we ',
|
|
413
|
+
'shall we ', 'shall i ',
|
|
414
|
+
'tell me '
|
|
415
|
+
]
|
|
416
|
+
return questionStarters.some(p => lower.startsWith(p))
|
|
417
|
+
}
|
|
330
418
|
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Brain Response Filter
|
|
3
|
-
* Phase 16: Filters noise, deduplicates, ranks, and synthesizes results
|
|
4
|
-
*
|
|
3
|
+
* Phase 16 + Phase 19: Filters noise, deduplicates, ranks, and synthesizes results
|
|
4
|
+
*
|
|
5
|
+
* Phase 19 changes:
|
|
6
|
+
* - D2: Lower word overlap threshold 0.85 → 0.75, add ID-based and prefix-based dedup
|
|
7
|
+
* - D3: Split INFRA_NOISE into strong (1 hit = filter) and weak (2+ hits = filter)
|
|
5
8
|
*/
|
|
6
9
|
|
|
7
10
|
export interface FilterableResult {
|
|
@@ -32,13 +35,19 @@ export interface TierResults {
|
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
export class ResponseFilter {
|
|
35
|
-
//
|
|
36
|
-
private readonly
|
|
38
|
+
// D3: Strong noise — 1 hit is enough to filter (very specific internal terms)
|
|
39
|
+
private readonly STRONG_NOISE = [
|
|
40
|
+
'mcp-server', 'model-context-protocol', 'embedding-service', 'vector-database',
|
|
41
|
+
'mcp tool', 'tool handler', 'precompute engine', 'knowledge graph builder',
|
|
42
|
+
'semantic cache', 'bun:test'
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
// D3: Weak noise — need 2+ hits to filter (terms that could appear in legitimate decisions)
|
|
46
|
+
private readonly WEAK_NOISE = [
|
|
37
47
|
'chromadb', 'chroma', 'minisearch', 'compromise', 'better-sqlite3',
|
|
38
|
-
'pino', 'hono', '
|
|
39
|
-
'
|
|
40
|
-
'
|
|
41
|
-
'semantic cache', 'precompute engine', 'knowledge graph builder'
|
|
48
|
+
'pino', 'hono', 'zod', 'claude-brain',
|
|
49
|
+
'phase 12', 'phase 13', 'phase 14', 'phase 15', 'phase 16',
|
|
50
|
+
'phase 17', 'phase 18', 'phase 19'
|
|
42
51
|
]
|
|
43
52
|
|
|
44
53
|
/**
|
|
@@ -52,7 +61,7 @@ export class ResponseFilter {
|
|
|
52
61
|
filtered = filtered.filter(r => !this.isInfrastructureNoise(r.content))
|
|
53
62
|
}
|
|
54
63
|
|
|
55
|
-
// 2. Remove near-duplicates (
|
|
64
|
+
// 2. Remove near-duplicates (Phase 19 D2: improved dedup)
|
|
56
65
|
filtered = this.deduplicateResults(filtered)
|
|
57
66
|
|
|
58
67
|
// 3. Apply dynamic threshold (at least 70% of median similarity)
|
|
@@ -134,32 +143,56 @@ export class ResponseFilter {
|
|
|
134
143
|
}
|
|
135
144
|
|
|
136
145
|
/**
|
|
137
|
-
* Check if content is infrastructure/internal noise
|
|
146
|
+
* D3: Check if content is infrastructure/internal noise
|
|
147
|
+
* Strong noise: 1 hit filters
|
|
148
|
+
* Weak noise: 2+ hits filters
|
|
138
149
|
*/
|
|
139
150
|
private isInfrastructureNoise(content: string): boolean {
|
|
140
151
|
const lower = content.toLowerCase()
|
|
141
|
-
let noiseHits = 0
|
|
142
152
|
|
|
143
|
-
|
|
153
|
+
// Strong noise: a single hit is enough
|
|
154
|
+
for (const term of this.STRONG_NOISE) {
|
|
144
155
|
if (lower.includes(term)) {
|
|
145
|
-
|
|
156
|
+
return true
|
|
146
157
|
}
|
|
147
158
|
}
|
|
148
159
|
|
|
149
|
-
//
|
|
150
|
-
|
|
160
|
+
// Weak noise: need 2+ hits
|
|
161
|
+
let weakHits = 0
|
|
162
|
+
for (const term of this.WEAK_NOISE) {
|
|
163
|
+
if (lower.includes(term)) {
|
|
164
|
+
weakHits++
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return weakHits >= 2
|
|
151
168
|
}
|
|
152
169
|
|
|
153
170
|
/**
|
|
154
|
-
*
|
|
171
|
+
* D2: Improved deduplication
|
|
172
|
+
* - ID-based dedup (if metadata has an ID)
|
|
173
|
+
* - Prefix-based dedup (first 100 chars match)
|
|
174
|
+
* - Word overlap dedup (lowered from 0.85 to 0.75)
|
|
155
175
|
*/
|
|
156
176
|
private deduplicateResults(results: FilterableResult[]): FilterableResult[] {
|
|
157
177
|
const kept: FilterableResult[] = []
|
|
178
|
+
const seenIds = new Set<string>()
|
|
179
|
+
const seenPrefixes = new Set<string>()
|
|
158
180
|
|
|
159
181
|
for (const result of results) {
|
|
182
|
+
// ID-based dedup
|
|
183
|
+
const id = (result.metadata as any)?.id || (result.metadata as any)?.decision_id
|
|
184
|
+
if (id && seenIds.has(id)) continue
|
|
185
|
+
if (id) seenIds.add(id)
|
|
186
|
+
|
|
187
|
+
// Prefix-based dedup (first 100 chars, normalized)
|
|
188
|
+
const prefix = result.content.slice(0, 100).toLowerCase().replace(/\s+/g, ' ').trim()
|
|
189
|
+
if (prefix.length > 20 && seenPrefixes.has(prefix)) continue
|
|
190
|
+
if (prefix.length > 20) seenPrefixes.add(prefix)
|
|
191
|
+
|
|
192
|
+
// Word overlap dedup (D2: lowered to 0.75)
|
|
160
193
|
const isDuplicate = kept.some(existing => {
|
|
161
194
|
const overlap = this.calculateWordOverlap(existing.content, result.content)
|
|
162
|
-
return overlap > 0.
|
|
195
|
+
return overlap > 0.75
|
|
163
196
|
})
|
|
164
197
|
|
|
165
198
|
if (!isDuplicate) {
|