claude-brain 0.9.1 → 0.9.3
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 +11 -0
- package/src/memory/index.ts +6 -3
- package/src/routing/intent-classifier.ts +15 -6
- package/src/routing/router.ts +98 -68
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.9.
|
|
1
|
+
0.9.3
|
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.9.
|
|
6
|
+
serverVersion: '0.9.3',
|
|
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.9.
|
|
273
|
+
serverVersion: z.string().regex(/^\d+\.\d+\.\d+$/, 'Version must be semver format').default('0.9.3'),
|
|
274
274
|
|
|
275
275
|
/** Logging level */
|
|
276
276
|
logLevel: LogLevelSchema.default('info'),
|
|
@@ -489,8 +489,19 @@ export class ChromaMemoryStore {
|
|
|
489
489
|
|
|
490
490
|
async deleteDecision(id: string): Promise<void> {
|
|
491
491
|
try {
|
|
492
|
+
// Delete from decisions collection
|
|
492
493
|
const collection = await this.collections.getDecisions()
|
|
493
494
|
await collection.delete({ ids: [id] })
|
|
495
|
+
|
|
496
|
+
// ALSO delete from memories collection (dual storage uses same ID)
|
|
497
|
+
try {
|
|
498
|
+
const memoriesCollection = await this.collections.getMemories()
|
|
499
|
+
await memoriesCollection.delete({ ids: [id] })
|
|
500
|
+
this.logger.debug({ id }, 'Decision also deleted from memories collection')
|
|
501
|
+
} catch {
|
|
502
|
+
// Memories collection entry may not exist, that's ok
|
|
503
|
+
}
|
|
504
|
+
|
|
494
505
|
this.logger.info({ id }, 'Decision deleted')
|
|
495
506
|
|
|
496
507
|
} catch (error) {
|
package/src/memory/index.ts
CHANGED
|
@@ -389,7 +389,8 @@ export class MemoryManager {
|
|
|
389
389
|
async fetchAllDecisions(project?: string): Promise<any[]> {
|
|
390
390
|
if (this.useChromaDB) {
|
|
391
391
|
try {
|
|
392
|
-
const
|
|
392
|
+
const collection = await this.chroma.collections.getDecisions()
|
|
393
|
+
const results = await collection.get({
|
|
393
394
|
where: project ? { project } : undefined
|
|
394
395
|
})
|
|
395
396
|
if (results && results.ids) {
|
|
@@ -419,7 +420,8 @@ export class MemoryManager {
|
|
|
419
420
|
async fetchAllPatterns(project?: string): Promise<any[]> {
|
|
420
421
|
if (this.useChromaDB) {
|
|
421
422
|
try {
|
|
422
|
-
const
|
|
423
|
+
const collection = await this.chroma.collections.getPatterns()
|
|
424
|
+
const results = await collection.get({
|
|
423
425
|
where: project ? { project } : undefined
|
|
424
426
|
})
|
|
425
427
|
if (results && results.ids) {
|
|
@@ -449,7 +451,8 @@ export class MemoryManager {
|
|
|
449
451
|
async fetchAllCorrections(project?: string): Promise<any[]> {
|
|
450
452
|
if (this.useChromaDB) {
|
|
451
453
|
try {
|
|
452
|
-
const
|
|
454
|
+
const collection = await this.chroma.collections.getCorrections()
|
|
455
|
+
const results = await collection.get({
|
|
453
456
|
where: project ? { project } : undefined
|
|
454
457
|
})
|
|
455
458
|
if (results && results.ids) {
|
|
@@ -173,14 +173,14 @@ export class IntentClassifier {
|
|
|
173
173
|
return { primary: 'update_memory', confidence: 0.85, secondary }
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
-
// 4. store_this: explicit "remember:", "save this:", "I prefer"
|
|
177
|
-
if (this.isStoreThis(lower)) {
|
|
176
|
+
// 4. store_this: explicit "remember:", "save this:", "I prefer" (never for questions)
|
|
177
|
+
if (this.isStoreThis(lower, message)) {
|
|
178
178
|
if (this.hasDecisionSignal(lower)) secondary.push('decision_made')
|
|
179
179
|
return { primary: 'store_this', confidence: 0.90, secondary }
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
-
// 5. decision_made: decision phrases + reasoning
|
|
183
|
-
if (this.isDecisionMade(lower)) {
|
|
182
|
+
// 5. decision_made: decision phrases + reasoning (never for questions)
|
|
183
|
+
if (this.isDecisionMade(lower, message)) {
|
|
184
184
|
if (this.hasComparisonSignal(lower)) secondary.push('comparison')
|
|
185
185
|
return { primary: 'decision_made', confidence: 0.85, secondary }
|
|
186
186
|
}
|
|
@@ -242,11 +242,20 @@ export class IntentClassifier {
|
|
|
242
242
|
return NO_ACTION_PHRASES.some(p => lower === p || lower === p + '.' || lower === p + '!')
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
-
private isStoreThis(lower: string): boolean {
|
|
245
|
+
private isStoreThis(lower: string, original?: string): boolean {
|
|
246
|
+
// Questions are never storage requests — "Do I prefer X?" is a query, not "I prefer X"
|
|
247
|
+
if (original?.trim().endsWith('?')) return false
|
|
248
|
+
const firstWord = lower.split(/\s+/)[0] || ''
|
|
249
|
+
if (QUESTION_WORDS.includes(firstWord)) return false
|
|
246
250
|
return STORE_PHRASES.some(p => lower.includes(p))
|
|
247
251
|
}
|
|
248
252
|
|
|
249
|
-
private isDecisionMade(lower: string): boolean {
|
|
253
|
+
private isDecisionMade(lower: string, original?: string): boolean {
|
|
254
|
+
// Questions are never decisions — "Did I decide to use X?" is a query
|
|
255
|
+
if (original?.trim().endsWith('?')) return false
|
|
256
|
+
const firstWord = lower.split(/\s+/)[0] || ''
|
|
257
|
+
if (QUESTION_WORDS.includes(firstWord)) return false
|
|
258
|
+
|
|
250
259
|
const hasDecision = DECISION_PHRASES.some(p => lower.includes(p))
|
|
251
260
|
if (!hasDecision) return false
|
|
252
261
|
// Higher confidence if reasoning is also present
|
package/src/routing/router.ts
CHANGED
|
@@ -565,7 +565,8 @@ export class BrainRouter {
|
|
|
565
565
|
}
|
|
566
566
|
|
|
567
567
|
/**
|
|
568
|
-
* Update a previous decision —
|
|
568
|
+
* Update a previous decision — searches by content to find the right one, then replaces it.
|
|
569
|
+
* Uses lastStoredId only for generic "actually, X" with no descriptive subject.
|
|
569
570
|
*/
|
|
570
571
|
private async handleUpdateMemory(
|
|
571
572
|
message: string,
|
|
@@ -579,14 +580,52 @@ export class BrainRouter {
|
|
|
579
580
|
}
|
|
580
581
|
|
|
581
582
|
const memory = getMemoryService()
|
|
583
|
+
const topic = entities.topic || message
|
|
584
|
+
const hasSpecificContent = this.hasDescriptiveContent(message)
|
|
582
585
|
|
|
583
|
-
//
|
|
584
|
-
|
|
586
|
+
// If the message has specific content, search for the matching decision first
|
|
587
|
+
if (hasSpecificContent) {
|
|
588
|
+
try {
|
|
589
|
+
const results = await memory.searchRaw(topic, {
|
|
590
|
+
project: effectiveProject,
|
|
591
|
+
limit: 1,
|
|
592
|
+
minSimilarity: 0.3
|
|
593
|
+
})
|
|
594
|
+
|
|
595
|
+
const matchId = results[0]?.memory?.id || results[0]?.decision?.id
|
|
596
|
+
if (results.length > 0 && matchId) {
|
|
597
|
+
const oldId = matchId
|
|
598
|
+
const newId = await memory.updateDecision(
|
|
599
|
+
oldId,
|
|
600
|
+
effectiveProject,
|
|
601
|
+
`Updated: ${topic.slice(0, 200)}`,
|
|
602
|
+
message,
|
|
603
|
+
entities.reasoning || 'Updated via brain tool',
|
|
604
|
+
{ tags: entities.technologies.length > 0 ? entities.technologies : ['updated'] }
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
this.lastStoredId = newId
|
|
608
|
+
this.lastStoredProject = effectiveProject
|
|
609
|
+
|
|
610
|
+
const oldContent = results[0].decision?.decision || results[0].memory?.content?.slice(0, 100) || ''
|
|
611
|
+
return {
|
|
612
|
+
action: 'stored',
|
|
613
|
+
summary: `Updated decision`,
|
|
614
|
+
content: `Replaced: "${oldContent.slice(0, 80)}"\n\nWith:\n**New content:** ${message}`,
|
|
615
|
+
relevantItems: 1
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
} catch {
|
|
619
|
+
// Search failed, fall through
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Generic update ("actually, use X instead") — use lastStoredId
|
|
585
624
|
if (this.lastStoredId) {
|
|
586
625
|
const newId = await memory.updateDecision(
|
|
587
626
|
this.lastStoredId,
|
|
588
627
|
effectiveProject,
|
|
589
|
-
`Updated: ${
|
|
628
|
+
`Updated: ${topic.slice(0, 200)}`,
|
|
590
629
|
message,
|
|
591
630
|
entities.reasoning || 'Updated via brain tool',
|
|
592
631
|
{ tags: entities.technologies.length > 0 ? entities.technologies : ['updated'] }
|
|
@@ -603,44 +642,10 @@ export class BrainRouter {
|
|
|
603
642
|
}
|
|
604
643
|
}
|
|
605
644
|
|
|
606
|
-
// No recent ID — search for the most similar decision to update
|
|
607
|
-
try {
|
|
608
|
-
const results = await memory.searchRaw(entities.topic || message, {
|
|
609
|
-
project: effectiveProject,
|
|
610
|
-
limit: 1,
|
|
611
|
-
minSimilarity: 0.3
|
|
612
|
-
})
|
|
613
|
-
|
|
614
|
-
if (results.length > 0 && results[0].id) {
|
|
615
|
-
const oldId = results[0].id
|
|
616
|
-
const newId = await memory.updateDecision(
|
|
617
|
-
oldId,
|
|
618
|
-
effectiveProject,
|
|
619
|
-
`Updated: ${entities.topic || message.slice(0, 200)}`,
|
|
620
|
-
message,
|
|
621
|
-
entities.reasoning || 'Updated via brain tool',
|
|
622
|
-
{ tags: entities.technologies.length > 0 ? entities.technologies : ['updated'] }
|
|
623
|
-
)
|
|
624
|
-
|
|
625
|
-
this.lastStoredId = newId
|
|
626
|
-
this.lastStoredProject = effectiveProject
|
|
627
|
-
|
|
628
|
-
const oldContent = results[0].decision?.decision || results[0].content?.slice(0, 100) || ''
|
|
629
|
-
return {
|
|
630
|
-
action: 'stored',
|
|
631
|
-
summary: `Updated decision`,
|
|
632
|
-
content: `Replaced: "${oldContent.slice(0, 80)}"\n\nWith:\n**New content:** ${message}`,
|
|
633
|
-
relevantItems: 1
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
} catch {
|
|
637
|
-
// Search failed
|
|
638
|
-
}
|
|
639
|
-
|
|
640
645
|
// Fallback: just store as a new decision
|
|
641
646
|
const decisionId = await memory.rememberDecision(
|
|
642
647
|
effectiveProject,
|
|
643
|
-
|
|
648
|
+
topic.slice(0, 200),
|
|
644
649
|
message,
|
|
645
650
|
entities.reasoning || '',
|
|
646
651
|
{ tags: entities.technologies.length > 0 ? entities.technologies : ['updated'] }
|
|
@@ -658,7 +663,8 @@ export class BrainRouter {
|
|
|
658
663
|
}
|
|
659
664
|
|
|
660
665
|
/**
|
|
661
|
-
* Delete a memory —
|
|
666
|
+
* Delete a memory — searches by content to find the right one.
|
|
667
|
+
* Only uses lastStoredId for very generic requests like "forget that" with no descriptive words.
|
|
662
668
|
*/
|
|
663
669
|
private async handleDeleteMemory(
|
|
664
670
|
message: string,
|
|
@@ -672,8 +678,42 @@ export class BrainRouter {
|
|
|
672
678
|
}
|
|
673
679
|
|
|
674
680
|
const memory = getMemoryService()
|
|
681
|
+
const topic = entities.topic || message
|
|
682
|
+
|
|
683
|
+
// Check if the message has meaningful content to search by
|
|
684
|
+
// (strip common delete phrases to see if there's a real subject)
|
|
685
|
+
const hasSpecificContent = this.hasDescriptiveContent(message)
|
|
686
|
+
|
|
687
|
+
// If the user described what to delete, ALWAYS search by content first
|
|
688
|
+
if (hasSpecificContent) {
|
|
689
|
+
try {
|
|
690
|
+
const results = await memory.searchRaw(topic, {
|
|
691
|
+
project: effectiveProject,
|
|
692
|
+
limit: 3,
|
|
693
|
+
minSimilarity: 0.3
|
|
694
|
+
})
|
|
695
|
+
|
|
696
|
+
const matchId = results[0]?.memory?.id || results[0]?.decision?.id
|
|
697
|
+
if (results.length > 0 && matchId) {
|
|
698
|
+
const targetId = matchId
|
|
699
|
+
const content = results[0].decision?.decision || results[0].memory?.content?.slice(0, 100) || ''
|
|
675
700
|
|
|
676
|
-
|
|
701
|
+
await memory.deleteDecision(targetId)
|
|
702
|
+
if (this.lastStoredId === targetId) this.lastStoredId = null
|
|
703
|
+
|
|
704
|
+
return {
|
|
705
|
+
action: 'stored',
|
|
706
|
+
summary: `Deleted memory`,
|
|
707
|
+
content: `Deleted: "${content.slice(0, 100)}" (ID: ${targetId})`,
|
|
708
|
+
relevantItems: 0
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
} catch {
|
|
712
|
+
// Search failed, fall through
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// Generic request ("forget that", "delete that") — use lastStoredId
|
|
677
717
|
if (this.lastStoredId) {
|
|
678
718
|
try {
|
|
679
719
|
await memory.deleteDecision(this.lastStoredId)
|
|
@@ -686,36 +726,10 @@ export class BrainRouter {
|
|
|
686
726
|
relevantItems: 0
|
|
687
727
|
}
|
|
688
728
|
} catch {
|
|
689
|
-
// Deletion failed
|
|
729
|
+
// Deletion failed
|
|
690
730
|
}
|
|
691
731
|
}
|
|
692
732
|
|
|
693
|
-
// Search for the most similar decision to delete
|
|
694
|
-
try {
|
|
695
|
-
const results = await memory.searchRaw(entities.topic || message, {
|
|
696
|
-
project: effectiveProject,
|
|
697
|
-
limit: 1,
|
|
698
|
-
minSimilarity: 0.3
|
|
699
|
-
})
|
|
700
|
-
|
|
701
|
-
if (results.length > 0 && results[0].id) {
|
|
702
|
-
const targetId = results[0].id
|
|
703
|
-
const content = results[0].decision?.decision || results[0].content?.slice(0, 100) || ''
|
|
704
|
-
|
|
705
|
-
await memory.deleteDecision(targetId)
|
|
706
|
-
this.lastStoredId = null
|
|
707
|
-
|
|
708
|
-
return {
|
|
709
|
-
action: 'stored',
|
|
710
|
-
summary: `Deleted memory`,
|
|
711
|
-
content: `Deleted: "${content.slice(0, 100)}" (ID: ${targetId})`,
|
|
712
|
-
relevantItems: 0
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
} catch {
|
|
716
|
-
// Search failed
|
|
717
|
-
}
|
|
718
|
-
|
|
719
733
|
return {
|
|
720
734
|
action: 'none',
|
|
721
735
|
summary: 'No matching memory found to delete',
|
|
@@ -959,6 +973,22 @@ export class BrainRouter {
|
|
|
959
973
|
}
|
|
960
974
|
}
|
|
961
975
|
|
|
976
|
+
/**
|
|
977
|
+
* Check if a delete/update message has descriptive content beyond just the command words.
|
|
978
|
+
* "forget that" → false (generic), "forget the migrations note" → true (specific)
|
|
979
|
+
*/
|
|
980
|
+
private hasDescriptiveContent(message: string): boolean {
|
|
981
|
+
const COMMAND_WORDS = [
|
|
982
|
+
'forget', 'delete', 'remove', 'discard', 'erase', 'undo', 'clear', 'drop',
|
|
983
|
+
'actually', 'correction', 'update', 'change', 'replace', 'modify', 'revise',
|
|
984
|
+
'amend', 'override', 'scratch', 'no', 'wait', 'instead',
|
|
985
|
+
'that', 'this', 'the', 'it', 'about', 'memory', 'decision', 'to', 'a', 'an'
|
|
986
|
+
]
|
|
987
|
+
const words = message.toLowerCase().split(/[\s,;:.!?]+/).filter(w => w.length > 1)
|
|
988
|
+
const meaningfulWords = words.filter(w => !COMMAND_WORDS.includes(w))
|
|
989
|
+
return meaningfulWords.length >= 2
|
|
990
|
+
}
|
|
991
|
+
|
|
962
992
|
private generateTaskId(title: string): string {
|
|
963
993
|
return title
|
|
964
994
|
.toLowerCase()
|