claude-brain 0.4.1 → 0.5.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.4.1
1
+ 0.5.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-brain",
3
- "version": "0.4.1",
3
+ "version": "0.5.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",
package/src/cli/bin.ts CHANGED
@@ -26,6 +26,7 @@ function printHelp() {
26
26
  console.log()
27
27
 
28
28
  const commands = [
29
+ ['start', 'Start ChromaDB + MCP server'],
29
30
  ['serve', 'Start the MCP server (default)'],
30
31
  ['setup', 'Run interactive setup wizard'],
31
32
  ['install', 'Register as MCP server in Claude Code'],
@@ -55,12 +56,11 @@ function printHelp() {
55
56
  ` ${theme.primary('-h'.padEnd(12))} ${dimText('Show help')}`,
56
57
  '',
57
58
  theme.bold('Examples:'),
58
- ` ${dimText('claude-brain')} ${dimText('Start MCP server')}`,
59
- ` ${dimText('claude-brain setup')} ${dimText('Configure Claude Brain')}`,
60
- ` ${dimText('claude-brain install')} ${dimText('Register with Claude Code')}`,
61
- ` ${dimText('claude-brain update')} ${dimText('Update to latest version')}`,
62
- ` ${dimText('claude-brain chroma start')} ${dimText('Start ChromaDB server')}`,
63
- ` ${dimText('claude-brain health')} ${dimText('Check system health')}`,
59
+ ` ${dimText('claude-brain start')} ${dimText('Start ChromaDB + MCP server')}`,
60
+ ` ${dimText('claude-brain start --chroma-only')} ${dimText('Start only ChromaDB')}`,
61
+ ` ${dimText('claude-brain setup')} ${dimText('Configure Claude Brain')}`,
62
+ ` ${dimText('claude-brain install')} ${dimText('Register with Claude Code')}`,
63
+ ` ${dimText('claude-brain health')} ${dimText('Check system health')}`,
64
64
  '',
65
65
  theme.bold('Environment:'),
66
66
  ` ${theme.primary('CLAUDE_BRAIN_HOME')} ${dimText('Override home directory (default: ~/.claude-brain/')}`,
@@ -74,6 +74,12 @@ async function main() {
74
74
  const command = process.argv[2] || 'serve'
75
75
 
76
76
  switch (command) {
77
+ case 'start': {
78
+ const { runStart } = await import('./commands/start')
79
+ await runStart()
80
+ break
81
+ }
82
+
77
83
  case 'serve': {
78
84
  const { runServe } = await import('./commands/serve')
79
85
  await runServe()
@@ -25,18 +25,75 @@ function getChromaDataPath(): string {
25
25
  return paths.chroma
26
26
  }
27
27
 
28
- function isChromaCliInstalled(): boolean {
28
+ /**
29
+ * Find the chroma binary - checks PATH first, then common pip install locations
30
+ */
31
+ function findChromaBinary(): string | null {
32
+ // Try bare 'chroma' first (on PATH)
29
33
  try {
30
34
  execSync('chroma --version', { stdio: 'pipe', timeout: 5000 })
31
- return true
32
- } catch {
33
- return false
35
+ return 'chroma'
36
+ } catch {}
37
+
38
+ // Search common pip install locations
39
+ const { homedir } = require('os')
40
+ const home = homedir()
41
+ const candidates = [
42
+ join(home, 'Library', 'Python', '3.9', 'bin', 'chroma'),
43
+ join(home, 'Library', 'Python', '3.10', 'bin', 'chroma'),
44
+ join(home, 'Library', 'Python', '3.11', 'bin', 'chroma'),
45
+ join(home, 'Library', 'Python', '3.12', 'bin', 'chroma'),
46
+ join(home, 'Library', 'Python', '3.13', 'bin', 'chroma'),
47
+ join(home, '.local', 'bin', 'chroma'),
48
+ '/usr/local/bin/chroma',
49
+ '/opt/homebrew/bin/chroma',
50
+ ]
51
+
52
+ for (const candidate of candidates) {
53
+ try {
54
+ if (existsSync(candidate)) {
55
+ execSync(`"${candidate}" --version`, { stdio: 'pipe', timeout: 5000 })
56
+ return candidate
57
+ }
58
+ } catch {}
59
+ }
60
+
61
+ // Try finding via python -m site
62
+ try {
63
+ const sitePackages = execSync('python3 -c "import site; print(site.getusersitepackages())"', {
64
+ encoding: 'utf-8', stdio: 'pipe', timeout: 5000
65
+ }).trim()
66
+ // User site-packages is like /Users/x/Library/Python/3.9/lib/python/site-packages
67
+ // The bin is at the same level as lib
68
+ const binDir = sitePackages.replace(/\/lib\/.*/, '/bin')
69
+ const chromaPath = join(binDir, 'chroma')
70
+ if (existsSync(chromaPath)) {
71
+ execSync(`"${chromaPath}" --version`, { stdio: 'pipe', timeout: 5000 })
72
+ return chromaPath
73
+ }
74
+ } catch {}
75
+
76
+ return null
77
+ }
78
+
79
+ let _cachedChromaBinary: string | null | undefined = undefined
80
+
81
+ function getChromaBinary(): string | null {
82
+ if (_cachedChromaBinary === undefined) {
83
+ _cachedChromaBinary = findChromaBinary()
34
84
  }
85
+ return _cachedChromaBinary
86
+ }
87
+
88
+ function isChromaCliInstalled(): boolean {
89
+ return getChromaBinary() !== null
35
90
  }
36
91
 
37
92
  function getChromaVersion(): string {
93
+ const binary = getChromaBinary()
94
+ if (!binary) return 'unknown'
38
95
  try {
39
- return execSync('chroma --version', { encoding: 'utf-8', stdio: 'pipe', timeout: 5000 }).trim()
96
+ return execSync(`"${binary}" --version`, { encoding: 'utf-8', stdio: 'pipe', timeout: 5000 }).trim()
40
97
  } catch {
41
98
  return 'unknown'
42
99
  }
@@ -75,15 +132,17 @@ function getRunningPid(): number | null {
75
132
  }
76
133
 
77
134
  function isChromaReachable(): boolean {
78
- try {
79
- execSync(`curl -sf http://localhost:${DEFAULT_PORT}/api/v1/heartbeat`, {
80
- stdio: 'pipe',
81
- timeout: 3000
82
- })
83
- return true
84
- } catch {
85
- return false
135
+ // Try v2 API first (ChromaDB 1.x server), then v1 (older servers)
136
+ for (const apiVersion of ['v2', 'v1']) {
137
+ try {
138
+ execSync(`curl -sf http://localhost:${DEFAULT_PORT}/api/${apiVersion}/heartbeat`, {
139
+ stdio: 'pipe',
140
+ timeout: 3000
141
+ })
142
+ return true
143
+ } catch {}
86
144
  }
145
+ return false
87
146
  }
88
147
 
89
148
  async function sleep(ms: number): Promise<void> {
@@ -112,8 +171,9 @@ async function chromaStart(): Promise<void> {
112
171
  }
113
172
 
114
173
  // Check if chroma CLI is installed
115
- if (!isChromaCliInstalled()) {
116
- console.log(errorText('ChromaDB CLI is not installed.'))
174
+ const chromaBinary = getChromaBinary()
175
+ if (!chromaBinary) {
176
+ console.log(errorText('ChromaDB CLI is not installed or not found on PATH.'))
117
177
  console.log()
118
178
  console.log(dimText('Install it with:'))
119
179
  console.log(` ${theme.primary('pip install chromadb')}`)
@@ -126,12 +186,13 @@ async function chromaStart(): Promise<void> {
126
186
 
127
187
  const dataPath = getChromaDataPath()
128
188
 
189
+ console.log(dimText(`Binary: ${chromaBinary}`))
129
190
  console.log(dimText(`Data path: ${dataPath}`))
130
191
  console.log(dimText(`Port: ${DEFAULT_PORT}`))
131
192
  console.log()
132
193
 
133
194
  // Start ChromaDB server in background
134
- const child = spawn('chroma', ['run', '--path', dataPath, '--port', DEFAULT_PORT], {
195
+ const child = spawn(chromaBinary, ['run', '--path', dataPath, '--port', DEFAULT_PORT], {
135
196
  detached: true,
136
197
  stdio: ['ignore', 'pipe', 'pipe'],
137
198
  env: { ...process.env }
@@ -375,6 +436,75 @@ function printChromaHelp(): void {
375
436
  console.log()
376
437
  }
377
438
 
439
+ // ── Auto-start for serve command ──────────────────────────
440
+
441
+ /**
442
+ * Ensures ChromaDB is running before the MCP server starts.
443
+ * Returns true if ChromaDB is reachable after this call, false otherwise.
444
+ * Designed to be called from serve.ts — does not call process.exit().
445
+ */
446
+ export async function ensureChromaRunning(options?: { silent?: boolean }): Promise<boolean> {
447
+ const log = options?.silent ? () => {} : console.error.bind(console)
448
+
449
+ // Already running? Great.
450
+ if (isChromaReachable()) {
451
+ return true
452
+ }
453
+
454
+ // Already managed by us but not responding? Clean up stale PID.
455
+ const stalePid = getRunningPid()
456
+ if (stalePid) {
457
+ try {
458
+ process.kill(stalePid, 'SIGTERM')
459
+ await sleep(1000)
460
+ } catch {}
461
+ try { unlinkSync(getPidFilePath()) } catch {}
462
+ }
463
+
464
+ const chromaBinary = getChromaBinary()
465
+ if (!chromaBinary) {
466
+ log('[ChromaDB] Not installed — running with SQLite fallback.')
467
+ log('[ChromaDB] Install with: claude-brain chroma install')
468
+ return false
469
+ }
470
+
471
+ const dataPath = getChromaDataPath()
472
+ log(`[ChromaDB] Starting server (port ${DEFAULT_PORT})...`)
473
+
474
+ const child = spawn(chromaBinary, ['run', '--path', dataPath, '--port', DEFAULT_PORT], {
475
+ detached: true,
476
+ stdio: ['ignore', 'pipe', 'pipe'],
477
+ env: { ...process.env }
478
+ })
479
+
480
+ const pid = child.pid
481
+ if (!pid) {
482
+ log('[ChromaDB] Failed to spawn server process.')
483
+ return false
484
+ }
485
+
486
+ child.unref()
487
+ writeFileSync(getPidFilePath(), String(pid), 'utf-8')
488
+
489
+ // Wait for server to become reachable (up to 15 seconds)
490
+ for (let i = 0; i < 30; i++) {
491
+ await sleep(500)
492
+ if (isChromaReachable()) {
493
+ log(`[ChromaDB] Server started (PID: ${pid})`)
494
+ return true
495
+ }
496
+ // Check if process died
497
+ try {
498
+ process.kill(pid, 0)
499
+ } catch {
500
+ break
501
+ }
502
+ }
503
+
504
+ log('[ChromaDB] Server started but not yet responding — will retry connection during initialization.')
505
+ return isChromaReachable()
506
+ }
507
+
378
508
  // ── Entry Point ───────────────────────────────────────────
379
509
 
380
510
  export async function runChroma(): Promise<void> {
@@ -5,6 +5,7 @@ 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'
8
9
 
9
10
  const BANNER = `
10
11
  ╔═══════════════════════════════════════════════════════╗
@@ -42,6 +43,11 @@ export async function runServe() {
42
43
  cacheSize: config.cacheSize
43
44
  }, 'Configuration loaded')
44
45
 
46
+ // Auto-start ChromaDB if not already running
47
+ mainLogger.info('Ensuring ChromaDB is available...')
48
+ const chromaReady = await ensureChromaRunning({ silent: process.env.NODE_ENV === 'production' })
49
+ mainLogger.info({ chromaReady }, chromaReady ? 'ChromaDB is ready' : 'ChromaDB not available, using SQLite fallback')
50
+
45
51
  mainLogger.info('Initializing services...')
46
52
  await initializeServices(config, logger)
47
53
  mainLogger.info('Services initialized successfully')
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Start Command
3
+ * Starts ChromaDB + MCP server together, or just ChromaDB with --chroma-only
4
+ *
5
+ * Usage:
6
+ * claude-brain start Start ChromaDB + MCP server
7
+ * claude-brain start --chroma-only Start only ChromaDB server
8
+ */
9
+
10
+ import {
11
+ heading, successText, warningText, dimText,
12
+ } from '@/cli/ui/index.js'
13
+ import { ensureChromaRunning } from '@/cli/commands/chroma'
14
+
15
+ export async function runStart(): Promise<void> {
16
+ const chromaOnly = process.argv.includes('--chroma-only')
17
+
18
+ if (chromaOnly) {
19
+ console.log()
20
+ console.log(heading('Starting ChromaDB'))
21
+ console.log()
22
+
23
+ const chromaReady = await ensureChromaRunning()
24
+ console.log()
25
+
26
+ if (chromaReady) {
27
+ console.log(successText('ChromaDB is running and ready.'))
28
+ } else {
29
+ console.log(warningText('ChromaDB could not be started. Check installation with: claude-brain chroma status'))
30
+ }
31
+ console.log()
32
+ return
33
+ }
34
+
35
+ // Full start: ChromaDB + MCP server
36
+ // serve.ts already calls ensureChromaRunning(), so just delegate
37
+ console.error(dimText('Starting ChromaDB + MCP server...'))
38
+ console.error()
39
+
40
+ const { runServe } = await import('./serve')
41
+ await runServe()
42
+ }
@@ -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.4.1',
6
+ serverVersion: '0.5.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.4.1'),
199
+ serverVersion: z.string().regex(/^\d+\.\d+\.\d+$/, 'Version must be semver format').default('0.5.0'),
200
200
 
201
201
  /** Logging level */
202
202
  logLevel: LogLevelSchema.default('info'),
@@ -29,8 +29,12 @@ export class ChromaClientManager {
29
29
 
30
30
  switch (this.config.mode) {
31
31
  case 'persistent':
32
+ // ChromaDB v3.x no longer supports embedded persistent mode via 'path'.
33
+ // Fall through to client-server connecting to localhost.
34
+ this.logger.info('Persistent mode: connecting to ChromaDB server on localhost (v3.x requires a running server)')
32
35
  this.client = new ChromaClient({
33
- path: this.config.path,
36
+ host: this.config.host || 'localhost',
37
+ port: this.config.port || 8000,
34
38
  ...authConfig
35
39
  })
36
40
  break
@@ -381,6 +381,97 @@ export class MemoryManager {
381
381
  return this.store.searchCorrections('', { limit: options?.limit || 10 })
382
382
  }
383
383
 
384
+ /**
385
+ * Fetch all decisions with content — routes to ChromaDB or SQLite
386
+ * Used by analytical tools that need bulk access to decision data
387
+ */
388
+ async fetchAllDecisions(project?: string): Promise<any[]> {
389
+ if (this.useChromaDB) {
390
+ try {
391
+ const results = await this.chroma.collections.decisions.get({
392
+ where: project ? { project } : undefined
393
+ })
394
+ if (results && results.ids) {
395
+ return results.ids.map((id: string, i: number) => ({
396
+ id,
397
+ content: results.documents?.[i] || '',
398
+ date: results.metadatas?.[i]?.created_at || new Date().toISOString(),
399
+ project: results.metadatas?.[i]?.project || project || 'unknown',
400
+ context: results.metadatas?.[i]?.context || '',
401
+ decision: results.metadatas?.[i]?.decision || results.documents?.[i] || '',
402
+ reasoning: results.metadatas?.[i]?.reasoning || '',
403
+ alternatives: results.metadatas?.[i]?.alternatives_considered || '',
404
+ tags: results.metadatas?.[i]?.tags || []
405
+ }))
406
+ }
407
+ return []
408
+ } catch (error) {
409
+ this.logger.warn({ error }, 'ChromaDB fetchAllDecisions failed, falling back to SQLite')
410
+ }
411
+ }
412
+ return this.store.getAllDecisionsWithContent(project)
413
+ }
414
+
415
+ /**
416
+ * Fetch all patterns with content — routes to ChromaDB or SQLite
417
+ */
418
+ async fetchAllPatterns(project?: string): Promise<any[]> {
419
+ if (this.useChromaDB) {
420
+ try {
421
+ const results = await this.chroma.collections.patterns.get({
422
+ where: project ? { project } : undefined
423
+ })
424
+ if (results && results.ids) {
425
+ return results.ids.map((id: string, i: number) => ({
426
+ id,
427
+ content: results.documents?.[i] || '',
428
+ date: results.metadatas?.[i]?.created_at || new Date().toISOString(),
429
+ project: results.metadatas?.[i]?.project || project || 'unknown',
430
+ pattern_type: results.metadatas?.[i]?.pattern_type || '',
431
+ description: results.metadatas?.[i]?.description || results.documents?.[i] || '',
432
+ example: results.metadatas?.[i]?.example || '',
433
+ confidence: results.metadatas?.[i]?.confidence || 0,
434
+ context: results.metadatas?.[i]?.context || ''
435
+ }))
436
+ }
437
+ return []
438
+ } catch (error) {
439
+ this.logger.warn({ error }, 'ChromaDB fetchAllPatterns failed, falling back to SQLite')
440
+ }
441
+ }
442
+ return this.store.getAllPatternsWithContent(project)
443
+ }
444
+
445
+ /**
446
+ * Fetch all corrections with content — routes to ChromaDB or SQLite
447
+ */
448
+ async fetchAllCorrections(project?: string): Promise<any[]> {
449
+ if (this.useChromaDB) {
450
+ try {
451
+ const results = await this.chroma.collections.corrections.get({
452
+ where: project ? { project } : undefined
453
+ })
454
+ if (results && results.ids) {
455
+ return results.ids.map((id: string, i: number) => ({
456
+ id,
457
+ content: results.documents?.[i] || '',
458
+ date: results.metadatas?.[i]?.created_at || new Date().toISOString(),
459
+ project: results.metadatas?.[i]?.project || project || 'unknown',
460
+ original: results.metadatas?.[i]?.original || '',
461
+ correction: results.metadatas?.[i]?.correction || results.documents?.[i] || '',
462
+ reasoning: results.metadatas?.[i]?.reasoning || '',
463
+ context: results.metadatas?.[i]?.context || '',
464
+ confidence: results.metadatas?.[i]?.confidence || 0
465
+ }))
466
+ }
467
+ return []
468
+ } catch (error) {
469
+ this.logger.warn({ error }, 'ChromaDB fetchAllCorrections failed, falling back to SQLite')
470
+ }
471
+ }
472
+ return this.store.getAllCorrectionsWithContent(project)
473
+ }
474
+
384
475
  /**
385
476
  * Search patterns by query — routes to ChromaDB or SQLite
386
477
  */
@@ -459,10 +459,14 @@ export class MemoryStore {
459
459
  metadata: {
460
460
  project: row.project,
461
461
  pattern_type: row.pattern_type,
462
+ description: row.description,
462
463
  example: row.example || '',
463
464
  confidence: row.confidence,
464
465
  context: row.context || '',
465
- created_at: row.created_at
466
+ created_at: row.created_at,
467
+ first_seen: row.created_at,
468
+ last_seen: row.created_at,
469
+ occurrences: 1
466
470
  }
467
471
  }))
468
472
  } catch (error) {
@@ -484,14 +488,19 @@ export class MemoryStore {
484
488
  return rows.map(row => ({
485
489
  id: row.id,
486
490
  correction: row.correction,
491
+ description: row.correction,
487
492
  metadata: {
488
493
  project: row.project,
489
494
  original: row.original,
490
495
  correction: row.correction,
496
+ description: row.correction,
491
497
  reasoning: row.reasoning,
492
498
  context: row.context || '',
493
499
  confidence: row.confidence,
494
- created_at: row.created_at
500
+ created_at: row.created_at,
501
+ first_seen: row.created_at,
502
+ last_seen: row.created_at,
503
+ occurrences: 1
495
504
  }
496
505
  }))
497
506
  } catch (error) {
@@ -553,10 +562,14 @@ export class MemoryStore {
553
562
  metadata: {
554
563
  project: row.project,
555
564
  pattern_type: row.pattern_type,
565
+ description: row.description,
556
566
  example: row.example || '',
557
567
  confidence: row.confidence,
558
568
  context: row.context || '',
559
- created_at: row.created_at
569
+ created_at: row.created_at,
570
+ first_seen: row.created_at,
571
+ last_seen: row.created_at,
572
+ occurrences: 1
560
573
  },
561
574
  similarity
562
575
  }
@@ -613,14 +626,19 @@ export class MemoryStore {
613
626
  return {
614
627
  id: row.id,
615
628
  content: row.correction,
629
+ description: row.correction,
616
630
  metadata: {
617
631
  project: row.project,
618
632
  original: row.original,
619
633
  correction: row.correction,
634
+ description: row.correction,
620
635
  reasoning: row.reasoning,
621
636
  context: row.context || '',
622
637
  confidence: row.confidence,
623
- created_at: row.created_at
638
+ created_at: row.created_at,
639
+ first_seen: row.created_at,
640
+ last_seen: row.created_at,
641
+ occurrences: 1
624
642
  },
625
643
  similarity
626
644
  }
@@ -636,6 +654,120 @@ export class MemoryStore {
636
654
  }
637
655
  }
638
656
 
657
+ /**
658
+ * Get all decisions with full content (for analytical tools without ChromaDB)
659
+ */
660
+ getAllDecisionsWithContent(project?: string): any[] {
661
+ try {
662
+ let sql = `
663
+ SELECT d.id, d.context, d.decision, d.reasoning, d.alternatives, d.tags,
664
+ m.content, m.project, m.created_at, m.metadata
665
+ FROM decisions d
666
+ JOIN memories m ON d.memory_id = m.id
667
+ `
668
+ const params: any[] = []
669
+ if (project) {
670
+ sql += ' WHERE m.project = ?'
671
+ params.push(project)
672
+ }
673
+ sql += ' ORDER BY m.created_at DESC'
674
+
675
+ const stmt = this.db.prepare(sql)
676
+ const rows = stmt.all(...params) as any[]
677
+
678
+ return rows.map(row => ({
679
+ id: row.id,
680
+ content: row.content,
681
+ date: row.created_at,
682
+ project: row.project,
683
+ context: row.context,
684
+ decision: row.decision,
685
+ reasoning: row.reasoning,
686
+ alternatives: row.alternatives,
687
+ tags: row.tags ? JSON.parse(row.tags) : []
688
+ }))
689
+ } catch (error) {
690
+ this.logger.error({ error, project }, 'Failed to get all decisions with content')
691
+ return []
692
+ }
693
+ }
694
+
695
+ /**
696
+ * Get all patterns with full content (for analytical tools without ChromaDB)
697
+ */
698
+ getAllPatternsWithContent(project?: string): any[] {
699
+ try {
700
+ let sql = `
701
+ SELECT p.id, p.pattern_type, p.description, p.example, p.confidence, p.context,
702
+ m.content, m.project, m.created_at
703
+ FROM patterns p
704
+ JOIN memories m ON p.memory_id = m.id
705
+ `
706
+ const params: any[] = []
707
+ if (project) {
708
+ sql += ' WHERE p.project = ?'
709
+ params.push(project)
710
+ }
711
+ sql += ' ORDER BY m.created_at DESC'
712
+
713
+ const stmt = this.db.prepare(sql)
714
+ const rows = stmt.all(...params) as any[]
715
+
716
+ return rows.map(row => ({
717
+ id: row.id,
718
+ content: row.content,
719
+ date: row.created_at,
720
+ project: row.project,
721
+ pattern_type: row.pattern_type,
722
+ description: row.description,
723
+ example: row.example,
724
+ confidence: row.confidence,
725
+ context: row.context
726
+ }))
727
+ } catch (error) {
728
+ this.logger.error({ error, project }, 'Failed to get all patterns with content')
729
+ return []
730
+ }
731
+ }
732
+
733
+ /**
734
+ * Get all corrections with full content (for analytical tools without ChromaDB)
735
+ */
736
+ getAllCorrectionsWithContent(project?: string): any[] {
737
+ try {
738
+ let sql = `
739
+ SELECT c.id, c.original, c.correction, c.reasoning, c.context as correction_context,
740
+ c.confidence, m.content, m.project, m.created_at
741
+ FROM corrections c
742
+ JOIN memories m ON c.memory_id = m.id
743
+ `
744
+ const params: any[] = []
745
+ if (project) {
746
+ sql += ' WHERE c.project = ?'
747
+ params.push(project)
748
+ }
749
+ sql += ' ORDER BY m.created_at DESC'
750
+
751
+ const stmt = this.db.prepare(sql)
752
+ const rows = stmt.all(...params) as any[]
753
+
754
+ return rows.map(row => ({
755
+ id: row.id,
756
+ content: row.content,
757
+ date: row.created_at,
758
+ project: row.project,
759
+ original: row.original,
760
+ correction: row.correction,
761
+ reasoning: row.reasoning,
762
+ context: row.correction_context,
763
+ confidence: row.confidence
764
+ }))
765
+ } catch (error) {
766
+ this.logger.error({ error, project }, 'Failed to get all corrections with content')
767
+ return []
768
+ }
769
+ }
770
+
639
771
  /**
640
772
  * Convert database row to Decision object
641
773
  */