claude-brain 0.22.0 → 0.22.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.22.0
1
+ 0.22.2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-brain",
3
- "version": "0.22.0",
3
+ "version": "0.22.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",
@@ -5,7 +5,6 @@ import { ClaudeBrainMCPServer } from '@/server'
5
5
  import { initializeServices, shutdownServices, getVaultService, getMemoryService } from '@/server/services'
6
6
  import { createOrchestrator, type Orchestrator } from '@/orchestrator'
7
7
  import { ensureHomeDirectory } from '@/cli/auto-setup'
8
- import { ensureChromaRunning } from '@/cli/commands/chroma'
9
8
 
10
9
  const BANNER = `
11
10
  ╔═══════════════════════════════════════════════════════╗
@@ -29,12 +28,12 @@ export async function runServe() {
29
28
  console.error(`[claude-brain] Hook auto-install skipped: ${error instanceof Error ? error.message : 'unknown error'}`)
30
29
  }
31
30
 
32
- if (process.env.NODE_ENV !== 'production') {
31
+ const config = await loadConfig()
32
+
33
+ if (config.logLevel === 'debug' || config.logLevel === 'info') {
33
34
  console.error(BANNER)
34
35
  }
35
36
 
36
- const config = await loadConfig()
37
-
38
37
  const logger = createLogger(config.logLevel, config.logFilePath)
39
38
  const mainLogger = createComponentLogger(logger, 'main')
40
39
 
@@ -54,10 +53,16 @@ export async function runServe() {
54
53
  cacheSize: config.cacheSize
55
54
  }, 'Configuration loaded')
56
55
 
57
- // Auto-start ChromaDB if not already running
58
- mainLogger.info('Ensuring ChromaDB is available...')
59
- const chromaReady = await ensureChromaRunning({ silent: process.env.NODE_ENV === 'production' })
60
- mainLogger.info({ chromaReady }, chromaReady ? 'ChromaDB is ready' : 'ChromaDB not available, using SQLite fallback')
56
+ // Only start ChromaDB if explicitly enabled in config
57
+ let chromaReady = false
58
+ if (config.chromadb?.enabled) {
59
+ mainLogger.info('ChromaDB enabled, ensuring it is available...')
60
+ const { ensureChromaRunning } = await import('@/cli/commands/chroma')
61
+ chromaReady = await ensureChromaRunning({ silent: process.env.NODE_ENV === 'production' })
62
+ mainLogger.info({ chromaReady }, chromaReady ? 'ChromaDB is ready' : 'ChromaDB not available, using SQLite fallback')
63
+ } else {
64
+ mainLogger.info('Using SQLite FTS5 storage (ChromaDB disabled)')
65
+ }
61
66
 
62
67
  mainLogger.info('Initializing services...')
63
68
  await initializeServices(config, logger)
@@ -1,17 +1,16 @@
1
1
  /**
2
2
  * Start Command
3
- * Starts ChromaDB + MCP server together, or just ChromaDB with --chroma-only
3
+ * Starts the Claude Brain server (MCP + HTTP)
4
4
  *
5
5
  * Usage:
6
- * claude-brain start Start ChromaDB + MCP server
7
- * claude-brain start --chroma-only Start only ChromaDB server
6
+ * claude-brain start Start server
7
+ * claude-brain start --chroma-only Start only ChromaDB server (if enabled)
8
8
  */
9
9
 
10
10
  import { parseArgs } from 'citty'
11
11
  import {
12
12
  heading, successText, warningText, dimText,
13
13
  } from '@/cli/ui/index.js'
14
- import { ensureChromaRunning } from '@/cli/commands/chroma'
15
14
 
16
15
  export async function runStart(): Promise<void> {
17
16
  const args = parseArgs(process.argv.slice(3), {
@@ -20,6 +19,7 @@ export async function runStart(): Promise<void> {
20
19
  const chromaOnly = args['chroma-only']
21
20
 
22
21
  if (chromaOnly) {
22
+ const { ensureChromaRunning } = await import('@/cli/commands/chroma')
23
23
  console.log()
24
24
  console.log(heading('Starting ChromaDB'))
25
25
  console.log()
@@ -36,9 +36,7 @@ export async function runStart(): Promise<void> {
36
36
  return
37
37
  }
38
38
 
39
- // Full start: ChromaDB + MCP server
40
- // serve.ts already calls ensureChromaRunning(), so just delegate
41
- console.error(dimText('Starting ChromaDB + MCP server...'))
39
+ console.error(dimText('Starting Claude Brain server...'))
42
40
  console.error()
43
41
 
44
42
  const { runServe } = await import('./serve')
@@ -39,16 +39,17 @@ export async function runStatus() {
39
39
  const { Database } = await import('bun:sqlite')
40
40
  const db = new Database(dbPath, { readonly: true })
41
41
 
42
- // Count observations by category
42
+ // Count observations by category (dynamic — includes all categories)
43
43
  const total = (db.prepare('SELECT COUNT(*) as cnt FROM observations WHERE archived = 0').get() as any)?.cnt ?? 0
44
- const decisions = (db.prepare("SELECT COUNT(*) as cnt FROM observations WHERE category = 'decision' AND archived = 0").get() as any)?.cnt ?? 0
45
- const patterns = (db.prepare("SELECT COUNT(*) as cnt FROM observations WHERE category = 'pattern' AND archived = 0").get() as any)?.cnt ?? 0
46
- const corrections = (db.prepare("SELECT COUNT(*) as cnt FROM observations WHERE category = 'correction' AND archived = 0").get() as any)?.cnt ?? 0
44
+ const catRows = db.prepare(
45
+ 'SELECT category, COUNT(*) as cnt FROM observations WHERE archived = 0 GROUP BY category'
46
+ ).all() as any[]
47
+ const breakdown = catRows.map((r: any) => `${r.cnt} ${r.category}s`).join(', ')
47
48
 
48
49
  items.push({ label: 'Storage', value: 'SQLite FTS5', status: 'success' })
49
50
  items.push({
50
51
  label: 'Observations',
51
- value: `${total} (${decisions} decisions, ${patterns} patterns, ${corrections} corrections)`,
52
+ value: `${total}${breakdown ? ` (${breakdown})` : ''}`,
52
53
  status: total > 0 ? 'success' : 'warning'
53
54
  })
54
55
 
@@ -79,7 +80,7 @@ export async function runStatus() {
79
80
  const symbols = (codeDb.prepare('SELECT COUNT(*) as cnt FROM code_symbols').get() as any)?.cnt ?? 0
80
81
 
81
82
  // Last indexed time
82
- const latest = (codeDb.prepare('SELECT MAX(indexed_at) as latest FROM code_files').get() as any)?.latest
83
+ const latest = (codeDb.prepare('SELECT MAX(last_indexed) as latest FROM code_files').get() as any)?.latest
83
84
  const lastIndexed = latest ? formatAge(latest) : 'never'
84
85
 
85
86
  items.push({
@@ -245,15 +245,21 @@ export class FTS5Search {
245
245
  /**
246
246
  * Fetch all observations for a project, optionally filtered by category.
247
247
  */
248
- fetchAll(project: string, category?: ObservationCategory): ObservationResult[] {
248
+ fetchAll(project?: string, category?: ObservationCategory): ObservationResult[] {
249
249
  let sql: string
250
- const params: any[] = [project]
250
+ const params: any[] = []
251
251
 
252
- if (category) {
252
+ if (project && category) {
253
253
  sql = `SELECT * FROM observations WHERE project = ? AND category = ? AND archived = 0 ORDER BY created_at DESC`
254
+ params.push(project, category)
255
+ } else if (project) {
256
+ sql = `SELECT * FROM observations WHERE project = ? AND archived = 0 ORDER BY created_at DESC`
257
+ params.push(project)
258
+ } else if (category) {
259
+ sql = `SELECT * FROM observations WHERE category = ? AND archived = 0 ORDER BY created_at DESC`
254
260
  params.push(category)
255
261
  } else {
256
- sql = `SELECT * FROM observations WHERE project = ? AND archived = 0 ORDER BY created_at DESC`
262
+ sql = `SELECT * FROM observations WHERE archived = 0 ORDER BY created_at DESC`
257
263
  }
258
264
 
259
265
  const rows = this.db.prepare(sql).all(...params) as any[]
@@ -337,6 +343,26 @@ export class FTS5Search {
337
343
  this.recordAccess(id)
338
344
  }
339
345
 
346
+ /**
347
+ * BUG-002: Search within a specific category, optionally scoped to a project.
348
+ * Returns results ordered by creation date (most recent first).
349
+ */
350
+ searchByCategory(category: ObservationCategory, project?: string, limit: number = 10): ObservationResult[] {
351
+ let sql: string
352
+ const params: any[] = []
353
+
354
+ if (project) {
355
+ sql = `SELECT * FROM observations WHERE category = ? AND project = ? AND archived = 0 ORDER BY created_at DESC LIMIT ?`
356
+ params.push(category, project, limit)
357
+ } else {
358
+ sql = `SELECT * FROM observations WHERE category = ? AND archived = 0 ORDER BY created_at DESC LIMIT ?`
359
+ params.push(category, limit)
360
+ }
361
+
362
+ const rows = this.db.prepare(sql).all(...params) as any[]
363
+ return rows.map(row => this.rowToResult(row))
364
+ }
365
+
340
366
  // --- Private helpers ---
341
367
 
342
368
  /**
@@ -362,9 +388,9 @@ export class FTS5Search {
362
388
  * Typical range: -20 (excellent) to 0 (poor).
363
389
  */
364
390
  private normalizeBM25(score: number): number {
365
- // Map typical BM25 range to 0.5-0.85
391
+ // Map BM25 range to 0.2-0.9 (wider range, allows low scores to be filtered)
366
392
  const normalized = Math.min(1, Math.max(0, score / 20))
367
- return 0.5 + normalized * 0.35
393
+ return 0.2 + normalized * 0.7
368
394
  }
369
395
 
370
396
  /**
@@ -543,8 +543,8 @@ export class MemoryManager {
543
543
  limit?: number
544
544
  }
545
545
  ): Promise<any[]> {
546
- // Phase 26: Try FTS5 first
547
- if (this._fts5 && project) {
546
+ // Phase 26: Try FTS5 first (BUG-002: works with or without project)
547
+ if (this._fts5) {
548
548
  const results = this._fts5.fetchAll(project, 'pattern')
549
549
  if (results.length > 0) {
550
550
  return results.map(r => ({
@@ -580,8 +580,8 @@ export class MemoryManager {
580
580
  project?: string,
581
581
  options?: { limit?: number }
582
582
  ): Promise<any[]> {
583
- // Phase 26: Try FTS5 first
584
- if (this._fts5 && project) {
583
+ // Phase 26: Try FTS5 first (BUG-002: works with or without project)
584
+ if (this._fts5) {
585
585
  const results = this._fts5.fetchAll(project, 'correction')
586
586
  if (results.length > 0) {
587
587
  return results.map(r => ({
@@ -615,8 +615,8 @@ export class MemoryManager {
615
615
  * Used by analytical tools that need bulk access to decision data
616
616
  */
617
617
  async fetchAllDecisions(project?: string): Promise<any[]> {
618
- // Phase 26: Try FTS5 first
619
- if (this._fts5 && project) {
618
+ // Phase 26: Try FTS5 first (BUG-002: works with or without project)
619
+ if (this._fts5) {
620
620
  const results = this._fts5.fetchAll(project, 'decision')
621
621
  if (results.length > 0) {
622
622
  return results.map(r => ({
@@ -664,8 +664,8 @@ export class MemoryManager {
664
664
  * Fetch all patterns with content — routes to FTS5, ChromaDB, or legacy SQLite
665
665
  */
666
666
  async fetchAllPatterns(project?: string): Promise<any[]> {
667
- // Phase 26: Try FTS5 first
668
- if (this._fts5 && project) {
667
+ // Phase 26: Try FTS5 first (BUG-002: works with or without project)
668
+ if (this._fts5) {
669
669
  const results = this._fts5.fetchAll(project, 'pattern')
670
670
  if (results.length > 0) {
671
671
  return results.map(r => ({
@@ -713,8 +713,8 @@ export class MemoryManager {
713
713
  * Fetch all corrections with content — routes to FTS5, ChromaDB, or legacy SQLite
714
714
  */
715
715
  async fetchAllCorrections(project?: string): Promise<any[]> {
716
- // Phase 26: Try FTS5 first
717
- if (this._fts5 && project) {
716
+ // Phase 26: Try FTS5 first (BUG-002: works with or without project)
717
+ if (this._fts5) {
718
718
  const results = this._fts5.fetchAll(project, 'correction')
719
719
  if (results.length > 0) {
720
720
  return results.map(r => ({
@@ -873,14 +873,47 @@ export class MemoryManager {
873
873
  reasoning: string,
874
874
  options?: { alternatives?: string; tags?: string[] }
875
875
  ): Promise<string> {
876
- // Delete old version fires onDecisionDeletedCallbacks (graph + episode cleanup)
876
+ // BUG-001: True in-place update using FTS5 (preserves original ID)
877
+ if (this._fts5) {
878
+ try {
879
+ this._fts5.update(oldId, {
880
+ content: decision,
881
+ reasoning,
882
+ context,
883
+ tags: options?.tags
884
+ })
885
+ this.logger.debug({ oldId }, 'Decision updated in-place via FTS5')
886
+
887
+ // Also update ChromaDB if available (best-effort)
888
+ if (this.useChromaDB) {
889
+ try {
890
+ await this.chroma.store.deleteDecision(oldId)
891
+ await this.chroma.store.storeDecision({
892
+ project,
893
+ context,
894
+ decision,
895
+ reasoning,
896
+ alternatives: options?.alternatives,
897
+ tags: options?.tags
898
+ })
899
+ } catch {
900
+ // ChromaDB sync failed, FTS5 is source of truth
901
+ }
902
+ }
903
+
904
+ return oldId // SAME ID preserved
905
+ } catch (error) {
906
+ this.logger.warn({ error, oldId }, 'FTS5 in-place update failed, falling back to delete+store')
907
+ }
908
+ }
909
+
910
+ // Fallback: delete + store (legacy behavior for non-FTS5 backends)
877
911
  try {
878
912
  await this.deleteDecision(oldId)
879
913
  this.logger.debug({ oldId }, 'Old decision deleted for update')
880
914
  } catch (error) {
881
915
  this.logger.warn({ error, oldId }, 'Failed to delete old decision during update, storing new version anyway')
882
916
  }
883
- // Store new version — fires onDecisionStoredCallbacks (graph rebuild)
884
917
  const newId = await this.rememberDecision(project, context, decision, reasoning, options)
885
918
  this.logger.debug({ oldId, newId }, 'Decision updated: old deleted, new stored')
886
919
  return newId
@@ -225,7 +225,7 @@ export class ResponseFilter {
225
225
  ? (left + right) / 2
226
226
  : right
227
227
 
228
- const threshold = Math.max(median * 0.7, 0.35)
228
+ const threshold = Math.max(median * 0.7, 0.45)
229
229
 
230
230
  return results.filter(r => r.score >= threshold)
231
231
  }
@@ -33,8 +33,8 @@ import type { ObservationCompressor } from '@/memory/compression'
33
33
  const DEFAULT_PROJECT = 'general'
34
34
 
35
35
  /** Phase 19 D4: Minimum similarity for destructive operations */
36
- const DELETE_MIN_SIMILARITY = 0.5
37
- const UPDATE_MIN_SIMILARITY = 0.5
36
+ const DELETE_MIN_SIMILARITY = 0.3
37
+ const UPDATE_MIN_SIMILARITY = 0.3
38
38
 
39
39
  export interface BrainInput {
40
40
  message: string
@@ -879,7 +879,23 @@ export class BrainRouter {
879
879
  relevantItems: 0
880
880
  }
881
881
  } else if (results.length > 0 && matchSimilarity < DELETE_MIN_SIMILARITY) {
882
- // D4: No confident match
882
+ // D4: No confident match — try direct FTS5 content search as fallback
883
+ const memory = getMemoryService()
884
+ if (memory.fts5) {
885
+ const ftsResults = memory.fts5.search(topic, effectiveProject, 3)
886
+ if (ftsResults.length > 0) {
887
+ const ftsTarget = ftsResults[0]
888
+ await memory.deleteDecision(ftsTarget.id)
889
+ if (this.lastStoredId === ftsTarget.id) this.lastStoredId = null
890
+ this.searchEngine.invalidateCache(effectiveProject)
891
+ return {
892
+ action: 'stored',
893
+ summary: `Deleted memory`,
894
+ content: `Deleted: "${ftsTarget.content.slice(0, 100)}" (ID: ${ftsTarget.id})`,
895
+ relevantItems: 0
896
+ }
897
+ }
898
+ }
883
899
  return {
884
900
  action: 'none',
885
901
  summary: 'No confident match to delete',
@@ -936,6 +952,12 @@ export class BrainRouter {
936
952
  return this.servicesNotReady()
937
953
  }
938
954
 
955
+ // BUG-002: Detect category-based queries and route to fetchAll
956
+ const categoryIntent = this.detectCategoryIntent(message)
957
+ if (categoryIntent) {
958
+ return this.handleCategoryQuery(message, project, categoryIntent)
959
+ }
960
+
939
961
  // Phase 25: Use undefined for search (no project filter = search all) when no project detected
940
962
  const searchProject = project || undefined
941
963
  const displayProject = project || DEFAULT_PROJECT
@@ -1832,6 +1854,102 @@ export class BrainRouter {
1832
1854
  return { content }
1833
1855
  }
1834
1856
 
1857
+ /**
1858
+ * BUG-002: Detect category-based intent from question messages.
1859
+ * Returns the category if the user is asking about a specific type of memory.
1860
+ */
1861
+ private detectCategoryIntent(message: string): 'decision' | 'pattern' | 'correction' | null {
1862
+ const lower = message.toLowerCase()
1863
+
1864
+ // Decision-oriented queries
1865
+ if (
1866
+ lower.includes('what decisions') || lower.includes('my decisions') ||
1867
+ lower.includes('what have i decided') || lower.includes('what did i decide') ||
1868
+ lower.includes('what choices') || lower.includes('decisions i') ||
1869
+ lower.includes('list decisions') || lower.includes('show decisions') ||
1870
+ lower.includes('all decisions') || lower.includes('what did i choose')
1871
+ ) {
1872
+ return 'decision'
1873
+ }
1874
+
1875
+ // Pattern-oriented queries
1876
+ if (
1877
+ lower.includes('what patterns') || lower.includes('my patterns') ||
1878
+ lower.includes('best practices') || lower.includes('conventions') ||
1879
+ lower.includes('list patterns') || lower.includes('show patterns') ||
1880
+ lower.includes('all patterns')
1881
+ ) {
1882
+ return 'pattern'
1883
+ }
1884
+
1885
+ // Correction-oriented queries
1886
+ if (
1887
+ lower.includes('what mistakes') || lower.includes('my mistakes') ||
1888
+ lower.includes('what bugs') || lower.includes('lessons learned') ||
1889
+ lower.includes('what corrections') || lower.includes('my corrections') ||
1890
+ lower.includes('list corrections') || lower.includes('show corrections') ||
1891
+ lower.includes('all corrections') || lower.includes('what have i fixed') ||
1892
+ lower.includes('what did i fix')
1893
+ ) {
1894
+ return 'correction'
1895
+ }
1896
+
1897
+ return null
1898
+ }
1899
+
1900
+ /**
1901
+ * BUG-002: Handle category-scoped queries by fetching all items of that category.
1902
+ */
1903
+ private async handleCategoryQuery(
1904
+ message: string,
1905
+ project: string | undefined,
1906
+ category: 'decision' | 'pattern' | 'correction'
1907
+ ): Promise<BrainResponse> {
1908
+ const memory = getMemoryService()
1909
+ const projectLabel = project || 'all projects'
1910
+
1911
+ let items: any[]
1912
+ switch (category) {
1913
+ case 'decision':
1914
+ items = await memory.fetchAllDecisions(project)
1915
+ break
1916
+ case 'pattern':
1917
+ items = await memory.fetchAllPatterns(project)
1918
+ break
1919
+ case 'correction':
1920
+ items = await memory.fetchAllCorrections(project)
1921
+ break
1922
+ }
1923
+
1924
+ if (items.length === 0) {
1925
+ return {
1926
+ action: 'none',
1927
+ summary: `No ${category}s found`,
1928
+ content: `No ${category}s found for ${projectLabel}.`,
1929
+ relevantItems: 0
1930
+ }
1931
+ }
1932
+
1933
+ // Format as compact items
1934
+ const allItems = items.map(item => ({
1935
+ id: item.id || item.decision_id,
1936
+ content: typeof (item.decision || item.description || item.correction || item.original || item.document || item.content) === 'string'
1937
+ ? (item.decision || item.description || item.correction || item.original || item.document || item.content)
1938
+ : JSON.stringify(item.decision || item.description || item.correction || item.original || item.document || item.content || ''),
1939
+ category,
1940
+ project: item.project || project || 'general',
1941
+ created_at: item.created_at || item.date || '',
1942
+ }))
1943
+
1944
+ const compactContent = formatCompactResponse(allItems, `${category}s for ${projectLabel}`)
1945
+ return {
1946
+ action: 'retrieved',
1947
+ summary: `${items.length} ${category}s for ${projectLabel}`,
1948
+ content: compactContent,
1949
+ relevantItems: items.length
1950
+ }
1951
+ }
1952
+
1835
1953
  private generateTaskId(title: string): string {
1836
1954
  return title
1837
1955
  .toLowerCase()
@@ -595,7 +595,7 @@ export class HttpApiServer {
595
595
  }
596
596
 
597
597
  const body = await c.req.json()
598
- const { projectPath, projectName } = body
598
+ const { projectPath, projectName, force } = body
599
599
 
600
600
  if (!projectPath || !projectName) {
601
601
  return Response.json(
@@ -604,6 +604,20 @@ export class HttpApiServer {
604
604
  )
605
605
  }
606
606
 
607
+ // BUG-004: Clear existing index data when force flag is set
608
+ if (force && this.codeQuery) {
609
+ try {
610
+ const db = (this.codeQuery as any).db
611
+ if (db) {
612
+ db.run('DELETE FROM code_files WHERE project = ?', projectName)
613
+ db.run('DELETE FROM code_symbols WHERE project = ?', projectName)
614
+ this.logger.info({ projectName }, 'Force reindex: cleared existing data')
615
+ }
616
+ } catch (error) {
617
+ this.logger.warn({ error, projectName }, 'Failed to clear index data for force reindex')
618
+ }
619
+ }
620
+
607
621
  const result = await this.codeIndexer.indexProject(projectPath, projectName)
608
622
  return Response.json({ success: true, data: result })
609
623
  } catch (error) {
@@ -654,7 +668,7 @@ export class HttpApiServer {
654
668
  )
655
669
  }
656
670
 
657
- const query = c.req.query('query') || ''
671
+ const query = c.req.query('query') || c.req.query('q') || ''
658
672
  const project = c.req.query('project') || ''
659
673
  const limit = parseInt(c.req.query('limit') || '20', 10)
660
674
 
@@ -600,7 +600,7 @@ export const TOOLS = {
600
600
  },
601
601
  project: {
602
602
  type: 'string',
603
- description: 'Project name (optional auto-detected from message if omitted)'
603
+ description: 'Project name (IMPORTANT: pass this to scope memories correctly, e.g. "my-app"). Auto-detected from message if omitted, defaults to "general".'
604
604
  }
605
605
  },
606
606
  required: ['message']