claude-brain 0.4.1 → 0.5.1

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.1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-brain",
3
- "version": "0.4.1",
3
+ "version": "0.5.1",
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,111 @@ 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
+ const isWindows = process.platform === 'win32'
33
+ const chromaName = isWindows ? 'chroma.exe' : 'chroma'
34
+
35
+ // Try bare 'chroma' first (on PATH)
29
36
  try {
30
37
  execSync('chroma --version', { stdio: 'pipe', timeout: 5000 })
31
- return true
32
- } catch {
33
- return false
38
+ return 'chroma'
39
+ } catch {}
40
+
41
+ const { homedir } = require('os')
42
+ const home = homedir()
43
+
44
+ // Platform-specific search paths
45
+ const candidates: string[] = isWindows
46
+ ? [
47
+ // Windows pip install locations
48
+ join(home, 'AppData', 'Local', 'Programs', 'Python', 'Python39', 'Scripts', chromaName),
49
+ join(home, 'AppData', 'Local', 'Programs', 'Python', 'Python310', 'Scripts', chromaName),
50
+ join(home, 'AppData', 'Local', 'Programs', 'Python', 'Python311', 'Scripts', chromaName),
51
+ join(home, 'AppData', 'Local', 'Programs', 'Python', 'Python312', 'Scripts', chromaName),
52
+ join(home, 'AppData', 'Local', 'Programs', 'Python', 'Python313', 'Scripts', chromaName),
53
+ // Windows user pip install (pip install --user)
54
+ join(home, 'AppData', 'Roaming', 'Python', 'Python39', 'Scripts', chromaName),
55
+ join(home, 'AppData', 'Roaming', 'Python', 'Python310', 'Scripts', chromaName),
56
+ join(home, 'AppData', 'Roaming', 'Python', 'Python311', 'Scripts', chromaName),
57
+ join(home, 'AppData', 'Roaming', 'Python', 'Python312', 'Scripts', chromaName),
58
+ join(home, 'AppData', 'Roaming', 'Python', 'Python313', 'Scripts', chromaName),
59
+ // Scoop / Chocolatey / winget
60
+ join(home, 'scoop', 'shims', chromaName),
61
+ 'C:\\Python39\\Scripts\\' + chromaName,
62
+ 'C:\\Python310\\Scripts\\' + chromaName,
63
+ 'C:\\Python311\\Scripts\\' + chromaName,
64
+ 'C:\\Python312\\Scripts\\' + chromaName,
65
+ 'C:\\Python313\\Scripts\\' + chromaName,
66
+ ]
67
+ : [
68
+ // macOS pip install locations
69
+ join(home, 'Library', 'Python', '3.9', 'bin', chromaName),
70
+ join(home, 'Library', 'Python', '3.10', 'bin', chromaName),
71
+ join(home, 'Library', 'Python', '3.11', 'bin', chromaName),
72
+ join(home, 'Library', 'Python', '3.12', 'bin', chromaName),
73
+ join(home, 'Library', 'Python', '3.13', 'bin', chromaName),
74
+ // Linux pip install locations
75
+ join(home, '.local', 'bin', chromaName),
76
+ '/usr/local/bin/' + chromaName,
77
+ '/opt/homebrew/bin/' + chromaName,
78
+ ]
79
+
80
+ for (const candidate of candidates) {
81
+ try {
82
+ if (existsSync(candidate)) {
83
+ execSync(`"${candidate}" --version`, { stdio: 'pipe', timeout: 5000 })
84
+ return candidate
85
+ }
86
+ } catch {}
34
87
  }
88
+
89
+ // Try finding via python -m site (works on all platforms)
90
+ const pythonCmd = isWindows ? 'python' : 'python3'
91
+ try {
92
+ const sitePackages = execSync(`${pythonCmd} -c "import site; print(site.getusersitepackages())"`, {
93
+ encoding: 'utf-8', stdio: 'pipe', timeout: 5000
94
+ }).trim()
95
+
96
+ let binDir: string
97
+ if (isWindows) {
98
+ // Windows: C:\Users\x\AppData\Roaming\Python\Python311\site-packages → Scripts
99
+ binDir = sitePackages.replace(/[\\\/]site-packages$/, '\\Scripts')
100
+ } else {
101
+ // Unix: /Users/x/Library/Python/3.9/lib/python/site-packages → bin
102
+ binDir = sitePackages.replace(/\/lib\/.*/, '/bin')
103
+ }
104
+
105
+ const chromaPath = join(binDir, chromaName)
106
+ if (existsSync(chromaPath)) {
107
+ execSync(`"${chromaPath}" --version`, { stdio: 'pipe', timeout: 5000 })
108
+ return chromaPath
109
+ }
110
+ } catch {}
111
+
112
+ return null
113
+ }
114
+
115
+ let _cachedChromaBinary: string | null | undefined = undefined
116
+
117
+ function getChromaBinary(): string | null {
118
+ if (_cachedChromaBinary === undefined) {
119
+ _cachedChromaBinary = findChromaBinary()
120
+ }
121
+ return _cachedChromaBinary
122
+ }
123
+
124
+ function isChromaCliInstalled(): boolean {
125
+ return getChromaBinary() !== null
35
126
  }
36
127
 
37
128
  function getChromaVersion(): string {
129
+ const binary = getChromaBinary()
130
+ if (!binary) return 'unknown'
38
131
  try {
39
- return execSync('chroma --version', { encoding: 'utf-8', stdio: 'pipe', timeout: 5000 }).trim()
132
+ return execSync(`"${binary}" --version`, { encoding: 'utf-8', stdio: 'pipe', timeout: 5000 }).trim()
40
133
  } catch {
41
134
  return 'unknown'
42
135
  }
@@ -75,15 +168,17 @@ function getRunningPid(): number | null {
75
168
  }
76
169
 
77
170
  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
171
+ // Try v2 API first (ChromaDB 1.x server), then v1 (older servers)
172
+ for (const apiVersion of ['v2', 'v1']) {
173
+ try {
174
+ execSync(`curl -sf http://localhost:${DEFAULT_PORT}/api/${apiVersion}/heartbeat`, {
175
+ stdio: 'pipe',
176
+ timeout: 3000
177
+ })
178
+ return true
179
+ } catch {}
86
180
  }
181
+ return false
87
182
  }
88
183
 
89
184
  async function sleep(ms: number): Promise<void> {
@@ -112,8 +207,9 @@ async function chromaStart(): Promise<void> {
112
207
  }
113
208
 
114
209
  // Check if chroma CLI is installed
115
- if (!isChromaCliInstalled()) {
116
- console.log(errorText('ChromaDB CLI is not installed.'))
210
+ const chromaBinary = getChromaBinary()
211
+ if (!chromaBinary) {
212
+ console.log(errorText('ChromaDB CLI is not installed or not found on PATH.'))
117
213
  console.log()
118
214
  console.log(dimText('Install it with:'))
119
215
  console.log(` ${theme.primary('pip install chromadb')}`)
@@ -126,12 +222,13 @@ async function chromaStart(): Promise<void> {
126
222
 
127
223
  const dataPath = getChromaDataPath()
128
224
 
225
+ console.log(dimText(`Binary: ${chromaBinary}`))
129
226
  console.log(dimText(`Data path: ${dataPath}`))
130
227
  console.log(dimText(`Port: ${DEFAULT_PORT}`))
131
228
  console.log()
132
229
 
133
230
  // Start ChromaDB server in background
134
- const child = spawn('chroma', ['run', '--path', dataPath, '--port', DEFAULT_PORT], {
231
+ const child = spawn(chromaBinary, ['run', '--path', dataPath, '--port', DEFAULT_PORT], {
135
232
  detached: true,
136
233
  stdio: ['ignore', 'pipe', 'pipe'],
137
234
  env: { ...process.env }
@@ -375,6 +472,75 @@ function printChromaHelp(): void {
375
472
  console.log()
376
473
  }
377
474
 
475
+ // ── Auto-start for serve command ──────────────────────────
476
+
477
+ /**
478
+ * Ensures ChromaDB is running before the MCP server starts.
479
+ * Returns true if ChromaDB is reachable after this call, false otherwise.
480
+ * Designed to be called from serve.ts — does not call process.exit().
481
+ */
482
+ export async function ensureChromaRunning(options?: { silent?: boolean }): Promise<boolean> {
483
+ const log = options?.silent ? () => {} : console.error.bind(console)
484
+
485
+ // Already running? Great.
486
+ if (isChromaReachable()) {
487
+ return true
488
+ }
489
+
490
+ // Already managed by us but not responding? Clean up stale PID.
491
+ const stalePid = getRunningPid()
492
+ if (stalePid) {
493
+ try {
494
+ process.kill(stalePid, 'SIGTERM')
495
+ await sleep(1000)
496
+ } catch {}
497
+ try { unlinkSync(getPidFilePath()) } catch {}
498
+ }
499
+
500
+ const chromaBinary = getChromaBinary()
501
+ if (!chromaBinary) {
502
+ log('[ChromaDB] Not installed — running with SQLite fallback.')
503
+ log('[ChromaDB] Install with: claude-brain chroma install')
504
+ return false
505
+ }
506
+
507
+ const dataPath = getChromaDataPath()
508
+ log(`[ChromaDB] Starting server (port ${DEFAULT_PORT})...`)
509
+
510
+ const child = spawn(chromaBinary, ['run', '--path', dataPath, '--port', DEFAULT_PORT], {
511
+ detached: true,
512
+ stdio: ['ignore', 'pipe', 'pipe'],
513
+ env: { ...process.env }
514
+ })
515
+
516
+ const pid = child.pid
517
+ if (!pid) {
518
+ log('[ChromaDB] Failed to spawn server process.')
519
+ return false
520
+ }
521
+
522
+ child.unref()
523
+ writeFileSync(getPidFilePath(), String(pid), 'utf-8')
524
+
525
+ // Wait for server to become reachable (up to 15 seconds)
526
+ for (let i = 0; i < 30; i++) {
527
+ await sleep(500)
528
+ if (isChromaReachable()) {
529
+ log(`[ChromaDB] Server started (PID: ${pid})`)
530
+ return true
531
+ }
532
+ // Check if process died
533
+ try {
534
+ process.kill(pid, 0)
535
+ } catch {
536
+ break
537
+ }
538
+ }
539
+
540
+ log('[ChromaDB] Server started but not yet responding — will retry connection during initialization.')
541
+ return isChromaReachable()
542
+ }
543
+
378
544
  // ── Entry Point ───────────────────────────────────────────
379
545
 
380
546
  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.1',
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.1'),
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
@@ -88,7 +92,7 @@ export class ChromaClientManager {
88
92
  }
89
93
 
90
94
  } catch (error) {
91
- this.logger.error({ error }, 'Failed to initialize ChromaDB client')
95
+ this.logger.warn({ error }, 'ChromaDB not available, will use SQLite fallback')
92
96
  throw error
93
97
  }
94
98
  }
@@ -54,7 +54,7 @@ export class ChromaManager {
54
54
  this.logger.info('ChromaDB initialized successfully')
55
55
 
56
56
  } catch (error) {
57
- this.logger.error({ error }, 'Failed to initialize ChromaDB')
57
+ this.logger.warn({ error }, 'ChromaDB unavailable')
58
58
  throw error
59
59
  }
60
60
  }
@@ -6,6 +6,25 @@ import type { DecisionMetadata, MemoryMetadata } from './schemas'
6
6
  import type { EmbeddingProvider } from './embeddings'
7
7
  import type { SearchResult } from './search'
8
8
 
9
+ /**
10
+ * Sanitize metadata for ChromaDB v3.x compatibility.
11
+ * Strips undefined/null values (ChromaDB only accepts string, number, boolean).
12
+ */
13
+ function sanitizeMetadata(metadata: Record<string, any>): Record<string, string | number | boolean> {
14
+ const clean: Record<string, string | number | boolean> = {}
15
+ for (const [key, value] of Object.entries(metadata)) {
16
+ if (value === undefined || value === null) continue
17
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
18
+ clean[key] = value
19
+ } else if (Array.isArray(value)) {
20
+ clean[key] = JSON.stringify(value)
21
+ } else {
22
+ clean[key] = String(value)
23
+ }
24
+ }
25
+ return clean
26
+ }
27
+
9
28
  export interface StoreDecisionInput {
10
29
  project: string
11
30
  context: string
@@ -145,7 +164,7 @@ export class ChromaMemoryStore {
145
164
  await collection.add({
146
165
  ids: [id],
147
166
  documents: [input.decision],
148
- metadatas: [metadata as Record<string, any>],
167
+ metadatas: [sanitizeMetadata(metadata as Record<string, any>)],
149
168
  ...(embeddings ? { embeddings } : {})
150
169
  })
151
170
 
@@ -166,7 +185,7 @@ export class ChromaMemoryStore {
166
185
  await memoriesCollection.add({
167
186
  ids: [id], // Use same ID for cross-reference
168
187
  documents: [memoryContent],
169
- metadatas: [memoryMetadata],
188
+ metadatas: [sanitizeMetadata(memoryMetadata)],
170
189
  ...(embeddings ? { embeddings } : {})
171
190
  })
172
191
 
@@ -230,7 +249,7 @@ export class ChromaMemoryStore {
230
249
  await collection.add({
231
250
  ids: [id],
232
251
  documents: [input.description],
233
- metadatas: [metadata],
252
+ metadatas: [sanitizeMetadata(metadata)],
234
253
  ...(embeddings ? { embeddings } : {})
235
254
  })
236
255
 
@@ -252,7 +271,7 @@ export class ChromaMemoryStore {
252
271
  await memoriesCollection.add({
253
272
  ids: [id],
254
273
  documents: [memoryContent],
255
- metadatas: [memoryMetadata],
274
+ metadatas: [sanitizeMetadata(memoryMetadata)],
256
275
  ...(embeddings ? { embeddings } : {})
257
276
  })
258
277
 
@@ -304,7 +323,7 @@ export class ChromaMemoryStore {
304
323
  await collection.add({
305
324
  ids: [id],
306
325
  documents: [input.correction],
307
- metadatas: [metadata],
326
+ metadatas: [sanitizeMetadata(metadata)],
308
327
  ...(embeddings ? { embeddings } : {})
309
328
  })
310
329
 
@@ -325,7 +344,7 @@ export class ChromaMemoryStore {
325
344
  await memoriesCollection.add({
326
345
  ids: [id],
327
346
  documents: [memoryContent],
328
- metadatas: [memoryMetadata],
347
+ metadatas: [sanitizeMetadata(memoryMetadata)],
329
348
  ...(embeddings ? { embeddings } : {})
330
349
  })
331
350
 
@@ -367,7 +386,7 @@ export class ChromaMemoryStore {
367
386
  await collection.add({
368
387
  ids: [id],
369
388
  documents: [input.content],
370
- metadatas: [metadata as Record<string, any>],
389
+ metadatas: [sanitizeMetadata(metadata as Record<string, any>)],
371
390
  ...(embeddings ? { embeddings } : {})
372
391
  })
373
392
 
@@ -409,7 +428,7 @@ export class ChromaMemoryStore {
409
428
  await collection.upsert({
410
429
  ids: [id],
411
430
  documents: [input.decision],
412
- metadatas: [metadata as Record<string, any>],
431
+ metadatas: [sanitizeMetadata(metadata as Record<string, any>)],
413
432
  ...(embeddings ? { embeddings } : {})
414
433
  })
415
434
 
@@ -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
  */