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 CHANGED
@@ -1 +1 @@
1
- 0.9.3
1
+ 0.10.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-brain",
3
- "version": "0.9.3",
3
+ "version": "0.10.0",
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.3',
6
+ serverVersion: '0.10.0',
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.3'),
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({
@@ -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
- return chromaResults.map(r => ({
248
- memory: {
249
- id: r.id,
250
- project: r.metadata.project || options?.project || 'unknown',
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 || (typeof r.content === 'string' ? r.content : ''),
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
- return { primary: 'question', confidence: 0.80, secondary }
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
- return SESSION_START_PHRASES.some(p => lower.includes(p))
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
- return QUESTION_WORDS.includes(firstWord)
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
- * from the unified brain() tool
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
- // Infrastructure noise terms strip from non-claude-brain projects
36
- private readonly INFRA_NOISE = [
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', 'bun:test', 'zod', 'mcp-server', 'claude-brain',
39
- 'model-context-protocol', 'embedding-service', 'vector-database',
40
- 'mcp tool', 'tool handler', 'phase 12', 'phase 13', 'phase 14', 'phase 15',
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 (>85% word overlap, keep higher score)
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
- for (const term of this.INFRA_NOISE) {
153
+ // Strong noise: a single hit is enough
154
+ for (const term of this.STRONG_NOISE) {
144
155
  if (lower.includes(term)) {
145
- noiseHits++
156
+ return true
146
157
  }
147
158
  }
148
159
 
149
- // If more than 2 infrastructure terms appear, it's likely noise
150
- return noiseHits >= 2
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
- * Remove near-duplicate results (>85% word overlap)
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.85
195
+ return overlap > 0.75
163
196
  })
164
197
 
165
198
  if (!isDuplicate) {