claude-brain 0.9.1 → 0.9.2
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/routing/intent-classifier.ts +15 -6
- package/src/routing/router.ts +96 -68
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.9.
|
|
1
|
+
0.9.2
|
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.2',
|
|
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.2'),
|
|
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) {
|
|
@@ -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,51 @@ 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
|
+
if (results.length > 0 && results[0].id) {
|
|
596
|
+
const oldId = results[0].id
|
|
597
|
+
const newId = await memory.updateDecision(
|
|
598
|
+
oldId,
|
|
599
|
+
effectiveProject,
|
|
600
|
+
`Updated: ${topic.slice(0, 200)}`,
|
|
601
|
+
message,
|
|
602
|
+
entities.reasoning || 'Updated via brain tool',
|
|
603
|
+
{ tags: entities.technologies.length > 0 ? entities.technologies : ['updated'] }
|
|
604
|
+
)
|
|
605
|
+
|
|
606
|
+
this.lastStoredId = newId
|
|
607
|
+
this.lastStoredProject = effectiveProject
|
|
608
|
+
|
|
609
|
+
const oldContent = results[0].decision?.decision || results[0].content?.slice(0, 100) || ''
|
|
610
|
+
return {
|
|
611
|
+
action: 'stored',
|
|
612
|
+
summary: `Updated decision`,
|
|
613
|
+
content: `Replaced: "${oldContent.slice(0, 80)}"\n\nWith:\n**New content:** ${message}`,
|
|
614
|
+
relevantItems: 1
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
} catch {
|
|
618
|
+
// Search failed, fall through
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Generic update ("actually, use X instead") — use lastStoredId
|
|
585
623
|
if (this.lastStoredId) {
|
|
586
624
|
const newId = await memory.updateDecision(
|
|
587
625
|
this.lastStoredId,
|
|
588
626
|
effectiveProject,
|
|
589
|
-
`Updated: ${
|
|
627
|
+
`Updated: ${topic.slice(0, 200)}`,
|
|
590
628
|
message,
|
|
591
629
|
entities.reasoning || 'Updated via brain tool',
|
|
592
630
|
{ tags: entities.technologies.length > 0 ? entities.technologies : ['updated'] }
|
|
@@ -603,44 +641,10 @@ export class BrainRouter {
|
|
|
603
641
|
}
|
|
604
642
|
}
|
|
605
643
|
|
|
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
644
|
// Fallback: just store as a new decision
|
|
641
645
|
const decisionId = await memory.rememberDecision(
|
|
642
646
|
effectiveProject,
|
|
643
|
-
|
|
647
|
+
topic.slice(0, 200),
|
|
644
648
|
message,
|
|
645
649
|
entities.reasoning || '',
|
|
646
650
|
{ tags: entities.technologies.length > 0 ? entities.technologies : ['updated'] }
|
|
@@ -658,7 +662,8 @@ export class BrainRouter {
|
|
|
658
662
|
}
|
|
659
663
|
|
|
660
664
|
/**
|
|
661
|
-
* Delete a memory —
|
|
665
|
+
* Delete a memory — searches by content to find the right one.
|
|
666
|
+
* Only uses lastStoredId for very generic requests like "forget that" with no descriptive words.
|
|
662
667
|
*/
|
|
663
668
|
private async handleDeleteMemory(
|
|
664
669
|
message: string,
|
|
@@ -672,8 +677,41 @@ export class BrainRouter {
|
|
|
672
677
|
}
|
|
673
678
|
|
|
674
679
|
const memory = getMemoryService()
|
|
680
|
+
const topic = entities.topic || message
|
|
681
|
+
|
|
682
|
+
// Check if the message has meaningful content to search by
|
|
683
|
+
// (strip common delete phrases to see if there's a real subject)
|
|
684
|
+
const hasSpecificContent = this.hasDescriptiveContent(message)
|
|
685
|
+
|
|
686
|
+
// If the user described what to delete, ALWAYS search by content first
|
|
687
|
+
if (hasSpecificContent) {
|
|
688
|
+
try {
|
|
689
|
+
const results = await memory.searchRaw(topic, {
|
|
690
|
+
project: effectiveProject,
|
|
691
|
+
limit: 3,
|
|
692
|
+
minSimilarity: 0.3
|
|
693
|
+
})
|
|
694
|
+
|
|
695
|
+
if (results.length > 0 && results[0].id) {
|
|
696
|
+
const targetId = results[0].id
|
|
697
|
+
const content = results[0].decision?.decision || results[0].content?.slice(0, 100) || ''
|
|
675
698
|
|
|
676
|
-
|
|
699
|
+
await memory.deleteDecision(targetId)
|
|
700
|
+
if (this.lastStoredId === targetId) this.lastStoredId = null
|
|
701
|
+
|
|
702
|
+
return {
|
|
703
|
+
action: 'stored',
|
|
704
|
+
summary: `Deleted memory`,
|
|
705
|
+
content: `Deleted: "${content.slice(0, 100)}" (ID: ${targetId})`,
|
|
706
|
+
relevantItems: 0
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
} catch {
|
|
710
|
+
// Search failed, fall through
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// Generic request ("forget that", "delete that") — use lastStoredId
|
|
677
715
|
if (this.lastStoredId) {
|
|
678
716
|
try {
|
|
679
717
|
await memory.deleteDecision(this.lastStoredId)
|
|
@@ -686,36 +724,10 @@ export class BrainRouter {
|
|
|
686
724
|
relevantItems: 0
|
|
687
725
|
}
|
|
688
726
|
} catch {
|
|
689
|
-
// Deletion failed
|
|
727
|
+
// Deletion failed
|
|
690
728
|
}
|
|
691
729
|
}
|
|
692
730
|
|
|
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
731
|
return {
|
|
720
732
|
action: 'none',
|
|
721
733
|
summary: 'No matching memory found to delete',
|
|
@@ -959,6 +971,22 @@ export class BrainRouter {
|
|
|
959
971
|
}
|
|
960
972
|
}
|
|
961
973
|
|
|
974
|
+
/**
|
|
975
|
+
* Check if a delete/update message has descriptive content beyond just the command words.
|
|
976
|
+
* "forget that" → false (generic), "forget the migrations note" → true (specific)
|
|
977
|
+
*/
|
|
978
|
+
private hasDescriptiveContent(message: string): boolean {
|
|
979
|
+
const COMMAND_WORDS = [
|
|
980
|
+
'forget', 'delete', 'remove', 'discard', 'erase', 'undo', 'clear', 'drop',
|
|
981
|
+
'actually', 'correction', 'update', 'change', 'replace', 'modify', 'revise',
|
|
982
|
+
'amend', 'override', 'scratch', 'no', 'wait', 'instead',
|
|
983
|
+
'that', 'this', 'the', 'it', 'about', 'memory', 'decision', 'to', 'a', 'an'
|
|
984
|
+
]
|
|
985
|
+
const words = message.toLowerCase().split(/[\s,;:.!?]+/).filter(w => w.length > 1)
|
|
986
|
+
const meaningfulWords = words.filter(w => !COMMAND_WORDS.includes(w))
|
|
987
|
+
return meaningfulWords.length >= 2
|
|
988
|
+
}
|
|
989
|
+
|
|
962
990
|
private generateTaskId(title: string): string {
|
|
963
991
|
return title
|
|
964
992
|
.toLowerCase()
|