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 CHANGED
@@ -1 +1 @@
1
- 0.9.1
1
+ 0.9.3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-brain",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
4
4
  "description": "Local development assistant bridging Obsidian vaults with Claude Code via MCP",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -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.1',
6
+ serverVersion: '0.9.3',
7
7
  logLevel: 'info',
8
8
  logFilePath: './logs/claude-brain.log',
9
9
  dbPath: './data/memory.db',
@@ -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.1'),
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) {
@@ -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 results = await this.chroma.collections.decisions.get({
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 results = await this.chroma.collections.patterns.get({
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 results = await this.chroma.collections.corrections.get({
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
@@ -565,7 +565,8 @@ export class BrainRouter {
565
565
  }
566
566
 
567
567
  /**
568
- * Update a previous decision — finds the most recent matching memory and replaces it
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
- // Strategy: find the most recent similar decision, delete it, store the new one
584
- // First try the lastStoredId shortcut
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: ${entities.topic || message.slice(0, 200)}`,
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
- entities.topic || message.slice(0, 200),
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 — finds the most recent matching memory and removes it
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
- // First try the lastStoredId shortcut
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, try search approach
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()