claude-brain 0.3.6 → 0.4.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/README.md CHANGED
@@ -28,7 +28,7 @@ A locally-running development assistant that bridges Obsidian knowledge vaults w
28
28
  # Install globally (requires Bun)
29
29
  bun install -g claude-brain
30
30
 
31
- # Interactive setup (vault path, log level, installs ~/CLAUDE.md)
31
+ # Interactive setup (vault path, log level, installs ~/.claude/CLAUDE.md)
32
32
  claude-brain setup
33
33
 
34
34
  # Register with Claude Code
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.6
1
+ 0.4.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-brain",
3
- "version": "0.3.6",
3
+ "version": "0.4.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",
@@ -69,17 +69,19 @@ export async function runUpdate() {
69
69
  }
70
70
  console.log()
71
71
 
72
- // Step 2: Update CLAUDE.md
72
+ // Step 2: Update CLAUDE.md to ~/.claude/CLAUDE.md (where Claude Code reads it)
73
73
  const sourcePath = path.join(PACKAGE_ROOT, 'assets', 'CLAUDE.md')
74
- const destPath = path.join(os.homedir(), 'CLAUDE.md')
74
+ const claudeDir = path.join(os.homedir(), '.claude')
75
+ const destPath = path.join(claudeDir, 'CLAUDE.md')
75
76
 
76
77
  if (!existsSync(sourcePath)) {
77
78
  console.log(warningText(' CLAUDE.md asset not found in package, skipping'))
78
79
  } else if (!existsSync(destPath)) {
79
80
  await withSpinner('Installing CLAUDE.md', async () => {
81
+ await fs.mkdir(claudeDir, { recursive: true })
80
82
  await fs.copyFile(sourcePath, destPath)
81
83
  })
82
- console.log(successText(' CLAUDE.md installed to ~/CLAUDE.md'))
84
+ console.log(successText(' CLAUDE.md installed to ~/.claude/CLAUDE.md'))
83
85
  } else {
84
86
  const sourceContent = readFileSync(sourcePath, 'utf-8')
85
87
  const destContent = readFileSync(destPath, 'utf-8')
@@ -90,7 +92,21 @@ export async function runUpdate() {
90
92
  await withSpinner('Updating CLAUDE.md', async () => {
91
93
  await fs.copyFile(sourcePath, destPath)
92
94
  })
93
- console.log(successText(' CLAUDE.md updated to latest version'))
95
+ console.log(successText(' CLAUDE.md updated to ~/.claude/CLAUDE.md'))
96
+ }
97
+ }
98
+
99
+ // Clean up old ~/CLAUDE.md if it exists (from previous versions)
100
+ const oldDestPath = path.join(os.homedir(), 'CLAUDE.md')
101
+ if (existsSync(oldDestPath)) {
102
+ try {
103
+ const oldContent = readFileSync(oldDestPath, 'utf-8')
104
+ if (oldContent.includes('Claude Brain')) {
105
+ await fs.unlink(oldDestPath)
106
+ console.log(dimText(' Removed old ~/CLAUDE.md (migrated to ~/.claude/)'))
107
+ }
108
+ } catch {
109
+ // ignore cleanup errors
94
110
  }
95
111
  }
96
112
 
@@ -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.3.6',
6
+ serverVersion: '0.4.0',
7
7
  logLevel: 'info',
8
8
  logFilePath: './logs/claude-brain.log',
9
9
  dbPath: './data/memory.db',
@@ -196,7 +196,7 @@ export const ConfigSchema = z.object({
196
196
  serverName: z.string().default('claude-brain'),
197
197
 
198
198
  /** Server version in semver format */
199
- serverVersion: z.string().regex(/^\d+\.\d+\.\d+$/, 'Version must be semver format').default('0.3.6'),
199
+ serverVersion: z.string().regex(/^\d+\.\d+\.\d+$/, 'Version must be semver format').default('0.4.0'),
200
200
 
201
201
  /** Logging level */
202
202
  logLevel: LogLevelSchema.default('info'),
@@ -248,6 +248,26 @@ export class StandardsManager {
248
248
  'Ignoring error handling',
249
249
  'Deep nesting'
250
250
  ]
251
+ },
252
+ {
253
+ language: 'JavaScript',
254
+ version: 'ES2022+',
255
+ style: 'standard',
256
+ linting: {},
257
+ conventions: [
258
+ 'Use strict mode',
259
+ 'Prefer const over let',
260
+ 'Use async/await over promises',
261
+ 'Use optional chaining and nullish coalescing',
262
+ 'Add JSDoc comments for public APIs'
263
+ ],
264
+ antiPatterns: [
265
+ 'Using var',
266
+ 'Callback hell',
267
+ 'Ignoring error handling',
268
+ 'Deep nesting',
269
+ 'Mutating function arguments'
270
+ ]
251
271
  }
252
272
  ],
253
273
  frameworks: [],
@@ -82,8 +82,9 @@ export class ChromaClientManager {
82
82
  this.isConnected = true
83
83
  this.logger.info('ChromaDB client initialized successfully')
84
84
  } catch (heartbeatError) {
85
- this.logger.warn({ error: heartbeatError }, 'ChromaDB heartbeat failed, connection may not be fully established')
86
- this.isConnected = true
85
+ this.isConnected = false
86
+ this.logger.warn({ error: heartbeatError }, 'ChromaDB heartbeat failed')
87
+ throw new Error(`ChromaDB server unreachable: ${heartbeatError instanceof Error ? heartbeatError.message : String(heartbeatError)}`)
87
88
  }
88
89
 
89
90
  } catch (error) {
@@ -35,7 +35,14 @@ export function getChromaConfigFromEnv(): Partial<ChromaConfig> {
35
35
 
36
36
  if (process.env.CHROMA_HOST) {
37
37
  config.host = process.env.CHROMA_HOST
38
- config.mode = 'cloud'
38
+ // Only default to cloud if CHROMA_MODE isn't explicitly set
39
+ if (!process.env.CHROMA_MODE) {
40
+ config.mode = 'cloud'
41
+ }
42
+ }
43
+
44
+ if (process.env.CHROMA_MODE) {
45
+ config.mode = process.env.CHROMA_MODE as any
39
46
  }
40
47
 
41
48
  if (process.env.CHROMA_EMBEDDING_PROVIDER) {
@@ -55,6 +55,7 @@ export class MemoryManager {
55
55
  private logger: Logger
56
56
  private initialized: boolean = false
57
57
  private useChromaDB: boolean = true
58
+ private onDecisionStoredCallbacks: ((input: any) => void)[] = []
58
59
 
59
60
  constructor(
60
61
  dbPath: string,
@@ -176,6 +177,24 @@ export class MemoryManager {
176
177
  return this.database.healthCheck()
177
178
  }
178
179
 
180
+ /**
181
+ * Check if ChromaDB backend is enabled and connected
182
+ */
183
+ isChromaDBEnabled(): boolean {
184
+ return this.useChromaDB
185
+ }
186
+
187
+ /**
188
+ * Add a listener that fires when a decision is stored (from any backend)
189
+ */
190
+ addDecisionStoredListener(callback: (input: any) => void): void {
191
+ this.onDecisionStoredCallbacks.push(callback)
192
+ // Also wire to ChromaDB listener if available
193
+ if (this.useChromaDB) {
194
+ this.chroma.store.addDecisionStoredListener(callback)
195
+ }
196
+ }
197
+
179
198
  async rememberDecision(
180
199
  project: string,
181
200
  context: string,
@@ -193,7 +212,7 @@ export class MemoryManager {
193
212
  tags: options?.tags
194
213
  })
195
214
  }
196
- return this.store.storeDecision({
215
+ const id = await this.store.storeDecision({
197
216
  project,
198
217
  context,
199
218
  decision,
@@ -201,6 +220,13 @@ export class MemoryManager {
201
220
  alternatives: options?.alternatives,
202
221
  tags: options?.tags
203
222
  })
223
+ // Notify listeners (e.g., knowledge graph builder) for SQLite path
224
+ for (const cb of this.onDecisionStoredCallbacks) {
225
+ try {
226
+ cb({ project, context, decision, reasoning, alternatives: options?.alternatives, tags: options?.tags, id })
227
+ } catch {}
228
+ }
229
+ return id
204
230
  }
205
231
 
206
232
  /**
@@ -281,7 +307,7 @@ export class MemoryManager {
281
307
  }
282
308
 
283
309
  /**
284
- * Store a pattern in memory
310
+ * Store a pattern in memory — routes to ChromaDB or SQLite
285
311
  */
286
312
  async storePattern(input: {
287
313
  project: string
@@ -291,14 +317,14 @@ export class MemoryManager {
291
317
  confidence: number
292
318
  context?: string
293
319
  }): Promise<string> {
294
- if (!this.useChromaDB) {
295
- throw new Error('Pattern storage requires ChromaDB backend')
320
+ if (this.useChromaDB) {
321
+ return this.chroma.store.storePattern(input)
296
322
  }
297
- return this.chroma.store.storePattern(input)
323
+ return this.store.storePattern(input)
298
324
  }
299
325
 
300
326
  /**
301
- * Store a correction/lesson learned in memory
327
+ * Store a correction/lesson learned routes to ChromaDB or SQLite
302
328
  */
303
329
  async storeCorrection(input: {
304
330
  project: string
@@ -308,14 +334,14 @@ export class MemoryManager {
308
334
  context?: string
309
335
  confidence: number
310
336
  }): Promise<string> {
311
- if (!this.useChromaDB) {
312
- throw new Error('Correction storage requires ChromaDB backend')
337
+ if (this.useChromaDB) {
338
+ return this.chroma.store.storeCorrection(input)
313
339
  }
314
- return this.chroma.store.storeCorrection(input)
340
+ return this.store.storeCorrection(input)
315
341
  }
316
342
 
317
343
  /**
318
- * Get patterns for a project
344
+ * Get patterns for a project — routes to ChromaDB or SQLite
319
345
  */
320
346
  async getPatterns(
321
347
  project?: string,
@@ -324,35 +350,39 @@ export class MemoryManager {
324
350
  limit?: number
325
351
  }
326
352
  ): Promise<any[]> {
327
- if (!this.useChromaDB) {
328
- throw new Error('Pattern retrieval requires ChromaDB backend')
353
+ if (this.useChromaDB) {
354
+ if (project) {
355
+ return this.chroma.store.getPatternsByProject(project, options)
356
+ }
357
+ return this.chroma.store.searchPatterns('', { limit: options?.limit || 10 })
329
358
  }
330
359
  if (project) {
331
- return this.chroma.store.getPatternsByProject(project, options)
360
+ return this.store.getPatternsByProject(project, options)
332
361
  }
333
- // If no project specified, search all patterns
334
- return this.chroma.store.searchPatterns('', { limit: options?.limit || 10 })
362
+ return this.store.searchPatterns('', { limit: options?.limit || 10 })
335
363
  }
336
364
 
337
365
  /**
338
- * Get corrections for a project
366
+ * Get corrections for a project — routes to ChromaDB or SQLite
339
367
  */
340
368
  async getCorrections(
341
369
  project?: string,
342
370
  options?: { limit?: number }
343
371
  ): Promise<any[]> {
344
- if (!this.useChromaDB) {
345
- throw new Error('Correction retrieval requires ChromaDB backend')
372
+ if (this.useChromaDB) {
373
+ if (project) {
374
+ return this.chroma.store.getCorrectionsByProject(project, options?.limit || 10)
375
+ }
376
+ return this.chroma.store.searchCorrections('', { limit: options?.limit || 10 })
346
377
  }
347
378
  if (project) {
348
- return this.chroma.store.getCorrectionsByProject(project, options?.limit || 10)
379
+ return this.store.getCorrectionsByProject(project, options?.limit || 10)
349
380
  }
350
- // If no project specified, search all corrections
351
- return this.chroma.store.searchCorrections('', { limit: options?.limit || 10 })
381
+ return this.store.searchCorrections('', { limit: options?.limit || 10 })
352
382
  }
353
383
 
354
384
  /**
355
- * Search patterns by query
385
+ * Search patterns by query — routes to ChromaDB or SQLite
356
386
  */
357
387
  async searchPatterns(
358
388
  query: string,
@@ -363,14 +393,14 @@ export class MemoryManager {
363
393
  minSimilarity?: number
364
394
  }
365
395
  ): Promise<any[]> {
366
- if (!this.useChromaDB) {
367
- throw new Error('Pattern search requires ChromaDB backend')
396
+ if (this.useChromaDB) {
397
+ return this.chroma.store.searchPatterns(query, options)
368
398
  }
369
- return this.chroma.store.searchPatterns(query, options)
399
+ return this.store.searchPatterns(query, options)
370
400
  }
371
401
 
372
402
  /**
373
- * Search corrections by query
403
+ * Search corrections by query — routes to ChromaDB or SQLite
374
404
  */
375
405
  async searchCorrections(
376
406
  query: string,
@@ -380,10 +410,10 @@ export class MemoryManager {
380
410
  minSimilarity?: number
381
411
  }
382
412
  ): Promise<any[]> {
383
- if (!this.useChromaDB) {
384
- throw new Error('Correction search requires ChromaDB backend')
413
+ if (this.useChromaDB) {
414
+ return this.chroma.store.searchCorrections(query, options)
385
415
  }
386
- return this.chroma.store.searchCorrections(query, options)
416
+ return this.store.searchCorrections(query, options)
387
417
  }
388
418
  }
389
419
 
@@ -41,12 +41,44 @@ CREATE TABLE IF NOT EXISTS decisions (
41
41
  -- Index for decision lookups
42
42
  CREATE INDEX IF NOT EXISTS idx_decisions_memory
43
43
  ON decisions(memory_id);
44
+
45
+ -- Patterns table (Phase 12)
46
+ -- Stores reusable patterns with SQLite fallback
47
+ CREATE TABLE IF NOT EXISTS patterns (
48
+ id TEXT PRIMARY KEY,
49
+ memory_id TEXT NOT NULL,
50
+ project TEXT NOT NULL,
51
+ pattern_type TEXT NOT NULL CHECK(pattern_type IN ('solution','anti-pattern','best-practice','common-issue')),
52
+ description TEXT NOT NULL,
53
+ example TEXT,
54
+ confidence REAL DEFAULT 0.8,
55
+ context TEXT,
56
+ created_at TEXT NOT NULL,
57
+ FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE
58
+ );
59
+ CREATE INDEX IF NOT EXISTS idx_patterns_project ON patterns(project);
60
+
61
+ -- Corrections table (Phase 12)
62
+ -- Stores lessons learned with SQLite fallback
63
+ CREATE TABLE IF NOT EXISTS corrections (
64
+ id TEXT PRIMARY KEY,
65
+ memory_id TEXT NOT NULL,
66
+ project TEXT NOT NULL,
67
+ original TEXT NOT NULL,
68
+ correction TEXT NOT NULL,
69
+ reasoning TEXT NOT NULL,
70
+ context TEXT,
71
+ confidence REAL DEFAULT 0.9,
72
+ created_at TEXT NOT NULL,
73
+ FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE
74
+ );
75
+ CREATE INDEX IF NOT EXISTS idx_corrections_project ON corrections(project);
44
76
  `
45
77
 
46
78
  /**
47
79
  * Schema version for migrations
48
80
  */
49
- export const SCHEMA_VERSION = 1
81
+ export const SCHEMA_VERSION = 2
50
82
 
51
83
  /**
52
84
  * Check if schema needs migration
@@ -17,7 +17,7 @@ import type {
17
17
  DecisionRow
18
18
  } from './types'
19
19
  import { EmbeddingService } from './embeddings'
20
- import { embeddingToBuffer, bufferToEmbedding } from './embedding-utils'
20
+ import { embeddingToBuffer, bufferToEmbedding, cosineSimilarity } from './embedding-utils'
21
21
 
22
22
  export class MemoryStore {
23
23
  private db: Database
@@ -326,6 +326,316 @@ export class MemoryStore {
326
326
  }
327
327
  }
328
328
 
329
+ /**
330
+ * Store a pattern (creates memory + pattern record)
331
+ */
332
+ async storePattern(input: {
333
+ project: string
334
+ pattern_type: 'solution' | 'anti-pattern' | 'best-practice' | 'common-issue'
335
+ description: string
336
+ example?: string
337
+ confidence: number
338
+ context?: string
339
+ }): Promise<string> {
340
+ try {
341
+ const content = `Pattern (${input.pattern_type}): ${input.description}${input.context ? `\nContext: ${input.context}` : ''}${input.example ? `\nExample: ${input.example}` : ''}`
342
+
343
+ const memoryId = await this.storeMemory({
344
+ project: input.project,
345
+ content,
346
+ metadata: {
347
+ type: 'pattern',
348
+ pattern_type: input.pattern_type,
349
+ confidence: input.confidence
350
+ }
351
+ })
352
+
353
+ const patternId = randomUUID()
354
+ const now = new Date().toISOString()
355
+ const stmt = this.db.prepare(`
356
+ INSERT INTO patterns (id, memory_id, project, pattern_type, description, example, confidence, context, created_at)
357
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
358
+ `)
359
+
360
+ stmt.run(
361
+ patternId,
362
+ memoryId,
363
+ input.project,
364
+ input.pattern_type,
365
+ input.description,
366
+ input.example || null,
367
+ input.confidence,
368
+ input.context || null,
369
+ now
370
+ )
371
+
372
+ this.logger.info({ patternId, memoryId, project: input.project }, 'Pattern stored')
373
+ return patternId
374
+ } catch (error) {
375
+ this.logger.error({ error, input }, 'Failed to store pattern')
376
+ throw error
377
+ }
378
+ }
379
+
380
+ /**
381
+ * Store a correction (creates memory + correction record)
382
+ */
383
+ async storeCorrection(input: {
384
+ project: string
385
+ original: string
386
+ correction: string
387
+ reasoning: string
388
+ context?: string
389
+ confidence: number
390
+ }): Promise<string> {
391
+ try {
392
+ const content = `Correction: ${input.correction}\nOriginal: ${input.original}\nReasoning: ${input.reasoning}${input.context ? `\nContext: ${input.context}` : ''}`
393
+
394
+ const memoryId = await this.storeMemory({
395
+ project: input.project,
396
+ content,
397
+ metadata: {
398
+ type: 'correction',
399
+ confidence: input.confidence
400
+ }
401
+ })
402
+
403
+ const correctionId = randomUUID()
404
+ const now = new Date().toISOString()
405
+ const stmt = this.db.prepare(`
406
+ INSERT INTO corrections (id, memory_id, project, original, correction, reasoning, context, confidence, created_at)
407
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
408
+ `)
409
+
410
+ stmt.run(
411
+ correctionId,
412
+ memoryId,
413
+ input.project,
414
+ input.original,
415
+ input.correction,
416
+ input.reasoning,
417
+ input.context || null,
418
+ input.confidence,
419
+ now
420
+ )
421
+
422
+ this.logger.info({ correctionId, memoryId, project: input.project }, 'Correction stored')
423
+ return correctionId
424
+ } catch (error) {
425
+ this.logger.error({ error, input }, 'Failed to store correction')
426
+ throw error
427
+ }
428
+ }
429
+
430
+ /**
431
+ * Get patterns by project with optional type filter
432
+ */
433
+ getPatternsByProject(project: string, options?: {
434
+ pattern_type?: 'solution' | 'anti-pattern' | 'best-practice' | 'common-issue'
435
+ limit?: number
436
+ }): any[] {
437
+ try {
438
+ let sql = 'SELECT * FROM patterns WHERE project = ?'
439
+ const params: any[] = [project]
440
+
441
+ if (options?.pattern_type) {
442
+ sql += ' AND pattern_type = ?'
443
+ params.push(options.pattern_type)
444
+ }
445
+
446
+ sql += ' ORDER BY created_at DESC'
447
+
448
+ if (options?.limit) {
449
+ sql += ' LIMIT ?'
450
+ params.push(options.limit)
451
+ }
452
+
453
+ const stmt = this.db.prepare(sql)
454
+ const rows = stmt.all(...params) as any[]
455
+
456
+ return rows.map(row => ({
457
+ id: row.id,
458
+ description: row.description,
459
+ metadata: {
460
+ project: row.project,
461
+ pattern_type: row.pattern_type,
462
+ example: row.example || '',
463
+ confidence: row.confidence,
464
+ context: row.context || '',
465
+ created_at: row.created_at
466
+ }
467
+ }))
468
+ } catch (error) {
469
+ this.logger.error({ error, project }, 'Failed to get patterns by project')
470
+ return []
471
+ }
472
+ }
473
+
474
+ /**
475
+ * Get corrections by project
476
+ */
477
+ getCorrectionsByProject(project: string, limit: number = 10): any[] {
478
+ try {
479
+ const stmt = this.db.prepare(
480
+ 'SELECT * FROM corrections WHERE project = ? ORDER BY created_at DESC LIMIT ?'
481
+ )
482
+ const rows = stmt.all(project, limit) as any[]
483
+
484
+ return rows.map(row => ({
485
+ id: row.id,
486
+ correction: row.correction,
487
+ metadata: {
488
+ project: row.project,
489
+ original: row.original,
490
+ correction: row.correction,
491
+ reasoning: row.reasoning,
492
+ context: row.context || '',
493
+ confidence: row.confidence,
494
+ created_at: row.created_at
495
+ }
496
+ }))
497
+ } catch (error) {
498
+ this.logger.error({ error, project }, 'Failed to get corrections by project')
499
+ return []
500
+ }
501
+ }
502
+
503
+ /**
504
+ * Search patterns using semantic search on underlying memories
505
+ */
506
+ async searchPatterns(
507
+ query: string,
508
+ options?: {
509
+ project?: string
510
+ pattern_type?: 'solution' | 'anti-pattern' | 'best-practice' | 'common-issue'
511
+ limit?: number
512
+ minSimilarity?: number
513
+ }
514
+ ): Promise<any[]> {
515
+ try {
516
+ // If no query, return all patterns for the project
517
+ if (!query && options?.project) {
518
+ return this.getPatternsByProject(options.project, {
519
+ pattern_type: options?.pattern_type,
520
+ limit: options?.limit
521
+ })
522
+ }
523
+
524
+ // Generate embedding for semantic search
525
+ const queryEmbedding = await this.embeddings.generateEmbedding(query || 'pattern')
526
+
527
+ let sql = `
528
+ SELECT p.*, m.embedding, m.content as memory_content
529
+ FROM patterns p
530
+ JOIN memories m ON p.memory_id = m.id
531
+ WHERE 1=1
532
+ `
533
+ const params: any[] = []
534
+
535
+ if (options?.project) {
536
+ sql += ' AND p.project = ?'
537
+ params.push(options.project)
538
+ }
539
+ if (options?.pattern_type) {
540
+ sql += ' AND p.pattern_type = ?'
541
+ params.push(options.pattern_type)
542
+ }
543
+
544
+ const stmt = this.db.prepare(sql)
545
+ const rows = stmt.all(...params) as any[]
546
+
547
+ const results = rows.map(row => {
548
+ const embedding = bufferToEmbedding(row.embedding)
549
+ const similarity = cosineSimilarity(queryEmbedding, embedding)
550
+ return {
551
+ id: row.id,
552
+ content: row.description,
553
+ metadata: {
554
+ project: row.project,
555
+ pattern_type: row.pattern_type,
556
+ example: row.example || '',
557
+ confidence: row.confidence,
558
+ context: row.context || '',
559
+ created_at: row.created_at
560
+ },
561
+ similarity
562
+ }
563
+ })
564
+ .filter(r => r.similarity >= (options?.minSimilarity || 0.3))
565
+ .sort((a, b) => b.similarity - a.similarity)
566
+ .slice(0, options?.limit || 10)
567
+
568
+ return results
569
+ } catch (error) {
570
+ this.logger.error({ error, query }, 'Pattern search failed')
571
+ return []
572
+ }
573
+ }
574
+
575
+ /**
576
+ * Search corrections using semantic search on underlying memories
577
+ */
578
+ async searchCorrections(
579
+ query: string,
580
+ options?: {
581
+ project?: string
582
+ limit?: number
583
+ minSimilarity?: number
584
+ }
585
+ ): Promise<any[]> {
586
+ try {
587
+ // If no query, return all corrections for the project
588
+ if (!query && options?.project) {
589
+ return this.getCorrectionsByProject(options.project, options?.limit || 10)
590
+ }
591
+
592
+ const queryEmbedding = await this.embeddings.generateEmbedding(query || 'correction')
593
+
594
+ let sql = `
595
+ SELECT c.*, m.embedding, m.content as memory_content
596
+ FROM corrections c
597
+ JOIN memories m ON c.memory_id = m.id
598
+ WHERE 1=1
599
+ `
600
+ const params: any[] = []
601
+
602
+ if (options?.project) {
603
+ sql += ' AND c.project = ?'
604
+ params.push(options.project)
605
+ }
606
+
607
+ const stmt = this.db.prepare(sql)
608
+ const rows = stmt.all(...params) as any[]
609
+
610
+ const results = rows.map(row => {
611
+ const embedding = bufferToEmbedding(row.embedding)
612
+ const similarity = cosineSimilarity(queryEmbedding, embedding)
613
+ return {
614
+ id: row.id,
615
+ content: row.correction,
616
+ metadata: {
617
+ project: row.project,
618
+ original: row.original,
619
+ correction: row.correction,
620
+ reasoning: row.reasoning,
621
+ context: row.context || '',
622
+ confidence: row.confidence,
623
+ created_at: row.created_at
624
+ },
625
+ similarity
626
+ }
627
+ })
628
+ .filter(r => r.similarity >= (options?.minSimilarity || 0.3))
629
+ .sort((a, b) => b.similarity - a.similarity)
630
+ .slice(0, options?.limit || 10)
631
+
632
+ return results
633
+ } catch (error) {
634
+ this.logger.error({ error, query }, 'Correction search failed')
635
+ return []
636
+ }
637
+ }
638
+
329
639
  /**
330
640
  * Convert database row to Decision object
331
641
  */
@@ -35,7 +35,10 @@ export async function handleAnalyzeDecisionEvolution(
35
35
  })
36
36
 
37
37
  if (analysis.totalDecisions === 0) {
38
- return ResponseFormatter.text(`No decisions found for topic: "${topic}"`)
38
+ const chromaNote = !memory.isChromaDBEnabled()
39
+ ? '\n\nNote: ChromaDB is not connected. Decision evolution analysis requires ChromaDB for semantic search. Start a ChromaDB server or switch to persistent mode.'
40
+ : ''
41
+ return ResponseFormatter.text(`No decisions found for topic: "${topic}"` + chromaNote)
39
42
  }
40
43
 
41
44
  const parts: string[] = []
@@ -33,7 +33,10 @@ export async function handleDetectTrends(
33
33
  })
34
34
 
35
35
  if (analysis.totalDecisionsAnalyzed === 0) {
36
- return ResponseFormatter.text('No decisions found for trend analysis.')
36
+ const chromaNote = !memory.isChromaDBEnabled()
37
+ ? '\n\nNote: ChromaDB is not connected. Trend detection requires ChromaDB for semantic search across decisions. Start a ChromaDB server or switch to persistent mode.'
38
+ : ''
39
+ return ResponseFormatter.text('No decisions found for trend analysis.' + chromaNote)
37
40
  }
38
41
 
39
42
  const parts: string[] = []
@@ -50,7 +50,10 @@ export async function handleGetDecisionTimeline(
50
50
  })
51
51
 
52
52
  if (timeline.entries.length === 0) {
53
- return ResponseFormatter.text('No decisions found for the specified criteria.')
53
+ const chromaNote = !memory.isChromaDBEnabled()
54
+ ? '\n\nNote: ChromaDB is not connected. Decision timeline requires ChromaDB for semantic search. Start a ChromaDB server or switch to persistent mode.'
55
+ : ''
56
+ return ResponseFormatter.text('No decisions found for the specified criteria.' + chromaNote)
54
57
  }
55
58
 
56
59
  // Format timeline
@@ -35,7 +35,10 @@ export async function handleGetRecommendations(
35
35
  })
36
36
 
37
37
  if (result.recommendations.length === 0) {
38
- return ResponseFormatter.text(`No recommendations found for: "${query}"`)
38
+ const chromaNote = !memory.isChromaDBEnabled()
39
+ ? '\n\nNote: ChromaDB is not connected. Recommendations require ChromaDB for semantic search across patterns, corrections, and decisions. Start a ChromaDB server or switch to persistent mode.'
40
+ : ''
41
+ return ResponseFormatter.text(`No recommendations found for: "${query}"` + chromaNote)
39
42
  }
40
43
 
41
44
  const parts: string[] = []
@@ -204,7 +204,7 @@ export const GetRecommendationsSchema = z.object({
204
204
  })
205
205
 
206
206
  export const FindCrossProjectPatternsSchema = z.object({
207
- min_projects: z.number().int().min(2).max(50).optional().default(2),
207
+ min_projects: z.number().int().min(1).max(50).optional().default(2),
208
208
  limit: z.number().int().min(1).max(100).optional().default(20),
209
209
  query: z.string().optional()
210
210
  })
@@ -50,6 +50,18 @@ export async function handleUpdateProgress(
50
50
  completedAt: new Date()
51
51
  })
52
52
 
53
+ // Calculate and update completion percentage
54
+ const progressState = await context.progress.getProgress(project_name)
55
+ const totalTasks = progressState.completedTasks.length + progressState.currentTasks.length
56
+ const completionPercentage = totalTasks > 0
57
+ ? Math.round((progressState.completedTasks.length / totalTasks) * 100)
58
+ : 0
59
+
60
+ await context.progress.updateProgress(project_name, {
61
+ completionPercentage,
62
+ currentPhase: completionPercentage >= 100 ? 'complete' : 'active'
63
+ })
64
+
53
65
  const progressFile = await vault.reader.readMarkdownFile(projectPaths.progress)
54
66
  const updatedContent = updateNextStepsSection(progressFile.content, next_steps)
55
67
 
@@ -63,7 +63,10 @@ export async function handleWhatIfAnalysis(
63
63
  return ResponseFormatter.text(withMemoryIndicator(content, totalAffected))
64
64
  }
65
65
 
66
- return ResponseFormatter.text(content)
66
+ const chromaNote = !memory.isChromaDBEnabled()
67
+ ? '\n\nNote: ChromaDB is not connected. What-if analysis requires ChromaDB for semantic search across decisions. Start a ChromaDB server or switch to persistent mode.'
68
+ : ''
69
+ return ResponseFormatter.text(content + chromaNote)
67
70
 
68
71
  } catch (error) {
69
72
  ErrorHandler.logError(logger, error, { tool: 'what_if_analysis' })
@@ -157,7 +157,8 @@ export async function initializeServices(config: Config, logger: Logger): Promis
157
157
  }
158
158
 
159
159
  // Hook builder into decision storage for real-time graph population
160
- memory.chroma.store.addDecisionStoredListener((input) => {
160
+ // Uses MemoryManager listener so it works for both ChromaDB and SQLite paths
161
+ memory.addDecisionStoredListener((input) => {
161
162
  builder.processDecision(input)
162
163
  })
163
164
 
@@ -122,7 +122,7 @@ export class SetupWizard {
122
122
  {
123
123
  type: 'confirm',
124
124
  name: 'installClaudeMd',
125
- message: 'Install CLAUDE.md protocol file to ~/CLAUDE.md?',
125
+ message: 'Install CLAUDE.md protocol file to ~/.claude/CLAUDE.md?',
126
126
  initial: true
127
127
  }
128
128
  ])
@@ -244,7 +244,7 @@ SERVER_NAME=claude-brain
244
244
 
245
245
  private async confirmClaudeMdInstall(): Promise<boolean> {
246
246
  const sourcePath = path.join(PACKAGE_ROOT, 'assets', 'CLAUDE.md')
247
- const destPath = path.join(os.homedir(), 'CLAUDE.md')
247
+ const destPath = path.join(os.homedir(), '.claude', 'CLAUDE.md')
248
248
 
249
249
  if (!existsSync(sourcePath)) {
250
250
  console.log(warningText('CLAUDE.md asset not found, skipping'))
@@ -255,7 +255,7 @@ SERVER_NAME=claude-brain
255
255
  const { overwrite } = await prompts({
256
256
  type: 'confirm',
257
257
  name: 'overwrite',
258
- message: '~/CLAUDE.md already exists. Overwrite?',
258
+ message: '~/.claude/CLAUDE.md already exists. Overwrite?',
259
259
  initial: false,
260
260
  })
261
261
  if (!overwrite) {
@@ -269,7 +269,9 @@ SERVER_NAME=claude-brain
269
269
 
270
270
  private async copyClaudeMd(): Promise<void> {
271
271
  const sourcePath = path.join(PACKAGE_ROOT, 'assets', 'CLAUDE.md')
272
- const destPath = path.join(os.homedir(), 'CLAUDE.md')
272
+ const claudeDir = path.join(os.homedir(), '.claude')
273
+ const destPath = path.join(claudeDir, 'CLAUDE.md')
274
+ await fs.mkdir(claudeDir, { recursive: true })
273
275
  await fs.copyFile(sourcePath, destPath)
274
276
  }
275
277