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 CHANGED
@@ -1 +1 @@
1
- 0.9.1
1
+ 0.9.2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-brain",
3
- "version": "0.9.1",
3
+ "version": "0.9.2",
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.2',
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.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
@@ -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,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
- // 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
+ 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: ${entities.topic || message.slice(0, 200)}`,
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
- entities.topic || message.slice(0, 200),
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 — finds the most recent matching memory and removes it
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
- // First try the lastStoredId shortcut
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, try search approach
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()