claude-brain 0.4.0 → 0.4.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.0
1
+ 0.4.1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-brain",
3
- "version": "0.4.0",
3
+ "version": "0.4.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",
@@ -8,6 +8,14 @@ function getDefaultEnv(vaultPath: string): string {
8
8
  VAULT_PATH=${vaultPath}
9
9
  LOG_LEVEL=info
10
10
  NODE_ENV=production
11
+
12
+ # ChromaDB Configuration
13
+ # Start ChromaDB server: claude-brain chroma start
14
+ # Install ChromaDB: claude-brain chroma install
15
+ CHROMA_MODE=client-server
16
+ CHROMA_HOST=localhost
17
+ CHROMA_PORT=8000
18
+ CHROMA_EMBEDDING_PROVIDER=transformers
11
19
  `
12
20
  }
13
21
 
package/src/cli/bin.ts CHANGED
@@ -31,6 +31,7 @@ function printHelp() {
31
31
  ['install', 'Register as MCP server in Claude Code'],
32
32
  ['uninstall', 'Remove MCP server from Claude Code'],
33
33
  ['update', 'Update package and refresh CLAUDE.md'],
34
+ ['chroma', 'Manage ChromaDB server (start/stop/status)'],
34
35
  ['health', 'Run health checks'],
35
36
  ['diagnose', 'Run diagnostics'],
36
37
  ['version', 'Show version'],
@@ -58,6 +59,7 @@ function printHelp() {
58
59
  ` ${dimText('claude-brain setup')} ${dimText('Configure Claude Brain')}`,
59
60
  ` ${dimText('claude-brain install')} ${dimText('Register with Claude Code')}`,
60
61
  ` ${dimText('claude-brain update')} ${dimText('Update to latest version')}`,
62
+ ` ${dimText('claude-brain chroma start')} ${dimText('Start ChromaDB server')}`,
61
63
  ` ${dimText('claude-brain health')} ${dimText('Check system health')}`,
62
64
  '',
63
65
  theme.bold('Environment:'),
@@ -102,6 +104,12 @@ async function main() {
102
104
  break
103
105
  }
104
106
 
107
+ case 'chroma': {
108
+ const { runChroma } = await import('./commands/chroma')
109
+ await runChroma()
110
+ break
111
+ }
112
+
105
113
  case 'health': {
106
114
  const { runHealthCheck } = await import('@/health')
107
115
  await runHealthCheck()
@@ -0,0 +1,407 @@
1
+ /**
2
+ * ChromaDB Management Command
3
+ * Start, stop, and manage the ChromaDB server for Claude Brain
4
+ */
5
+
6
+ import { spawn, execSync } from 'node:child_process'
7
+ import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'node:fs'
8
+ import { join } from 'node:path'
9
+ import { getHomePaths } from '@/config/home'
10
+ import {
11
+ theme, heading, successText, errorText, warningText, dimText,
12
+ infoPanel,
13
+ } from '@/cli/ui/index.js'
14
+
15
+ const PID_FILENAME = 'chroma.pid'
16
+ const DEFAULT_PORT = '8000'
17
+
18
+ function getPidFilePath(): string {
19
+ const paths = getHomePaths()
20
+ return join(paths.data, PID_FILENAME)
21
+ }
22
+
23
+ function getChromaDataPath(): string {
24
+ const paths = getHomePaths()
25
+ return paths.chroma
26
+ }
27
+
28
+ function isChromaCliInstalled(): boolean {
29
+ try {
30
+ execSync('chroma --version', { stdio: 'pipe', timeout: 5000 })
31
+ return true
32
+ } catch {
33
+ return false
34
+ }
35
+ }
36
+
37
+ function getChromaVersion(): string {
38
+ try {
39
+ return execSync('chroma --version', { encoding: 'utf-8', stdio: 'pipe', timeout: 5000 }).trim()
40
+ } catch {
41
+ return 'unknown'
42
+ }
43
+ }
44
+
45
+ function isPythonInstalled(): { installed: boolean; cmd: string } {
46
+ for (const cmd of ['python3', 'python']) {
47
+ try {
48
+ execSync(`${cmd} --version`, { stdio: 'pipe', timeout: 5000 })
49
+ return { installed: true, cmd }
50
+ } catch {
51
+ // try next
52
+ }
53
+ }
54
+ return { installed: false, cmd: 'python3' }
55
+ }
56
+
57
+ function getRunningPid(): number | null {
58
+ const pidPath = getPidFilePath()
59
+ if (!existsSync(pidPath)) return null
60
+
61
+ try {
62
+ const pid = parseInt(readFileSync(pidPath, 'utf-8').trim(), 10)
63
+ if (isNaN(pid)) {
64
+ unlinkSync(pidPath)
65
+ return null
66
+ }
67
+ // Signal 0 tests if process exists without killing it
68
+ process.kill(pid, 0)
69
+ return pid
70
+ } catch {
71
+ // Process not running, clean up stale PID file
72
+ try { unlinkSync(pidPath) } catch {}
73
+ return null
74
+ }
75
+ }
76
+
77
+ 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
86
+ }
87
+ }
88
+
89
+ async function sleep(ms: number): Promise<void> {
90
+ return new Promise(resolve => setTimeout(resolve, ms))
91
+ }
92
+
93
+ // ── Subcommands ───────────────────────────────────────────
94
+
95
+ async function chromaStart(): Promise<void> {
96
+ console.log()
97
+ console.log(heading('Starting ChromaDB Server'))
98
+ console.log()
99
+
100
+ // Check if already running
101
+ const existingPid = getRunningPid()
102
+ if (existingPid) {
103
+ console.log(warningText(`ChromaDB is already running (PID: ${existingPid})`))
104
+ console.log()
105
+ return
106
+ }
107
+
108
+ if (isChromaReachable()) {
109
+ console.log(warningText('A ChromaDB server is already running on port ' + DEFAULT_PORT))
110
+ console.log()
111
+ return
112
+ }
113
+
114
+ // Check if chroma CLI is installed
115
+ if (!isChromaCliInstalled()) {
116
+ console.log(errorText('ChromaDB CLI is not installed.'))
117
+ console.log()
118
+ console.log(dimText('Install it with:'))
119
+ console.log(` ${theme.primary('pip install chromadb')}`)
120
+ console.log()
121
+ console.log(dimText('Or run:'))
122
+ console.log(` ${theme.primary('claude-brain chroma install')}`)
123
+ console.log()
124
+ process.exit(1)
125
+ }
126
+
127
+ const dataPath = getChromaDataPath()
128
+
129
+ console.log(dimText(`Data path: ${dataPath}`))
130
+ console.log(dimText(`Port: ${DEFAULT_PORT}`))
131
+ console.log()
132
+
133
+ // Start ChromaDB server in background
134
+ const child = spawn('chroma', ['run', '--path', dataPath, '--port', DEFAULT_PORT], {
135
+ detached: true,
136
+ stdio: ['ignore', 'pipe', 'pipe'],
137
+ env: { ...process.env }
138
+ })
139
+
140
+ const pid = child.pid
141
+ if (!pid) {
142
+ console.log(errorText('Failed to start ChromaDB server — no PID returned.'))
143
+ console.log()
144
+ process.exit(1)
145
+ }
146
+
147
+ // Detach the child process so it runs independently
148
+ child.unref()
149
+
150
+ // Save PID file
151
+ writeFileSync(getPidFilePath(), String(pid), 'utf-8')
152
+
153
+ // Capture early output for error detection
154
+ let startupOutput = ''
155
+ child.stderr?.on('data', (chunk: Buffer) => {
156
+ startupOutput += chunk.toString()
157
+ })
158
+ child.stdout?.on('data', (chunk: Buffer) => {
159
+ startupOutput += chunk.toString()
160
+ })
161
+
162
+ // Wait up to 15 seconds for server to become reachable
163
+ let ready = false
164
+ for (let i = 0; i < 30; i++) {
165
+ await sleep(500)
166
+ if (isChromaReachable()) {
167
+ ready = true
168
+ break
169
+ }
170
+ // Check if process exited early
171
+ try {
172
+ process.kill(pid, 0)
173
+ } catch {
174
+ break
175
+ }
176
+ }
177
+
178
+ if (ready) {
179
+ console.log(successText(`ChromaDB server started (PID: ${pid})`))
180
+ console.log()
181
+ console.log(dimText('The server is running in the background.'))
182
+ console.log(dimText('Stop it with: ') + theme.primary('claude-brain chroma stop'))
183
+ } else {
184
+ // Check if process is still alive
185
+ let alive = false
186
+ try {
187
+ process.kill(pid, 0)
188
+ alive = true
189
+ } catch {}
190
+
191
+ if (alive) {
192
+ console.log(warningText('ChromaDB server started but not yet responding.'))
193
+ console.log(dimText(`PID: ${pid} — it may still be initializing.`))
194
+ console.log(dimText('Check with: ') + theme.primary('claude-brain chroma status'))
195
+ } else {
196
+ console.log(errorText('ChromaDB server failed to start.'))
197
+ if (startupOutput) {
198
+ console.log(dimText('Output:'))
199
+ console.log(dimText(startupOutput.slice(0, 500)))
200
+ }
201
+ try { unlinkSync(getPidFilePath()) } catch {}
202
+ process.exit(1)
203
+ }
204
+ }
205
+ console.log()
206
+ }
207
+
208
+ async function chromaStop(): Promise<void> {
209
+ console.log()
210
+ console.log(heading('Stopping ChromaDB Server'))
211
+ console.log()
212
+
213
+ const pid = getRunningPid()
214
+ if (!pid) {
215
+ if (isChromaReachable()) {
216
+ console.log(warningText('A ChromaDB server is running on port ' + DEFAULT_PORT + ' but was not started by claude-brain.'))
217
+ console.log(dimText('Kill it manually if needed.'))
218
+ } else {
219
+ console.log(dimText('No ChromaDB server is currently running.'))
220
+ }
221
+ console.log()
222
+ return
223
+ }
224
+
225
+ try {
226
+ process.kill(pid, 'SIGTERM')
227
+
228
+ // Wait up to 5 seconds for graceful shutdown
229
+ for (let i = 0; i < 10; i++) {
230
+ await sleep(500)
231
+ try {
232
+ process.kill(pid, 0)
233
+ } catch {
234
+ // Process exited
235
+ break
236
+ }
237
+ }
238
+
239
+ // Force kill if still running
240
+ try {
241
+ process.kill(pid, 0)
242
+ // Still alive, force kill
243
+ process.kill(pid, 'SIGKILL')
244
+ } catch {
245
+ // Already dead, good
246
+ }
247
+
248
+ try { unlinkSync(getPidFilePath()) } catch {}
249
+
250
+ console.log(successText(`ChromaDB server stopped (PID: ${pid})`))
251
+ } catch (error) {
252
+ console.log(errorText(`Failed to stop ChromaDB: ${error instanceof Error ? error.message : String(error)}`))
253
+ try { unlinkSync(getPidFilePath()) } catch {}
254
+ }
255
+ console.log()
256
+ }
257
+
258
+ async function chromaStatus(): Promise<void> {
259
+ console.log()
260
+
261
+ const pid = getRunningPid()
262
+ const reachable = isChromaReachable()
263
+ const installed = isChromaCliInstalled()
264
+ const version = installed ? getChromaVersion() : 'N/A'
265
+ const dataPath = getChromaDataPath()
266
+ const dataExists = existsSync(dataPath)
267
+
268
+ const items: Record<string, string> = {
269
+ 'Installed': installed ? `Yes (${version})` : 'No',
270
+ 'Server': reachable ? `Running (port ${DEFAULT_PORT})` : 'Not running',
271
+ 'Managed PID': pid ? String(pid) : 'None',
272
+ 'Data Path': dataPath,
273
+ 'Data Exists': dataExists ? 'Yes' : 'No',
274
+ }
275
+
276
+ console.log(infoPanel('ChromaDB Status', items))
277
+
278
+ if (!installed) {
279
+ console.log()
280
+ console.log(warningText('ChromaDB is not installed.'))
281
+ console.log(dimText('Install with: ') + theme.primary('claude-brain chroma install'))
282
+ } else if (!reachable) {
283
+ console.log()
284
+ console.log(dimText('Start the server with: ') + theme.primary('claude-brain chroma start'))
285
+ }
286
+ console.log()
287
+ }
288
+
289
+ async function chromaInstall(): Promise<void> {
290
+ console.log()
291
+ console.log(heading('Installing ChromaDB'))
292
+ console.log()
293
+
294
+ if (isChromaCliInstalled()) {
295
+ const version = getChromaVersion()
296
+ console.log(successText(`ChromaDB is already installed (${version}).`))
297
+ console.log()
298
+ return
299
+ }
300
+
301
+ const python = isPythonInstalled()
302
+ if (!python.installed) {
303
+ console.log(errorText('Python 3 is required to install ChromaDB.'))
304
+ console.log()
305
+ console.log(dimText('Install Python 3 first:'))
306
+ console.log(` ${theme.primary('macOS:')} brew install python3`)
307
+ console.log(` ${theme.primary('Ubuntu:')} sudo apt install python3 python3-pip`)
308
+ console.log(` ${theme.primary('Windows:')} https://python.org/downloads`)
309
+ console.log()
310
+ process.exit(1)
311
+ }
312
+
313
+ console.log(dimText(`Found ${python.cmd}. Installing chromadb via pip...`))
314
+ console.log()
315
+
316
+ const pipCommands = python.cmd === 'python3'
317
+ ? ['pip3 install chromadb', 'python3 -m pip install chromadb']
318
+ : ['pip install chromadb', 'python -m pip install chromadb']
319
+
320
+ let installed = false
321
+ for (const cmd of pipCommands) {
322
+ try {
323
+ execSync(cmd, { stdio: 'inherit', timeout: 300_000 })
324
+ installed = true
325
+ break
326
+ } catch {
327
+ // Try next command
328
+ }
329
+ }
330
+
331
+ console.log()
332
+ if (installed) {
333
+ console.log(successText('ChromaDB installed successfully!'))
334
+ console.log()
335
+ console.log(dimText('Start the server with: ') + theme.primary('claude-brain chroma start'))
336
+ } else {
337
+ console.log(errorText('Failed to install ChromaDB.'))
338
+ console.log(dimText('Try installing manually:'))
339
+ console.log(` ${theme.primary('pip install chromadb')}`)
340
+ process.exit(1)
341
+ }
342
+ console.log()
343
+ }
344
+
345
+ // ── Help ──────────────────────────────────────────────────
346
+
347
+ function printChromaHelp(): void {
348
+ console.log()
349
+ console.log(heading('ChromaDB Management'))
350
+ console.log()
351
+ console.log(dimText('ChromaDB provides vector storage for semantic search,'))
352
+ console.log(dimText('knowledge graph, and advanced intelligence features.'))
353
+ console.log()
354
+
355
+ const commands = [
356
+ ['start', 'Start ChromaDB server in background'],
357
+ ['stop', 'Stop the running ChromaDB server'],
358
+ ['status', 'Show ChromaDB server status'],
359
+ ['install', 'Install ChromaDB (requires Python 3)'],
360
+ ]
361
+
362
+ const cmdLines = commands
363
+ .map(([cmd, desc]) => ` ${theme.primary(cmd!.padEnd(12))} ${dimText(desc!)}`)
364
+ .join('\n')
365
+
366
+ console.log(theme.bold('Commands:'))
367
+ console.log(cmdLines)
368
+ console.log()
369
+
370
+ console.log(theme.bold('Usage:'))
371
+ console.log(` ${dimText('claude-brain chroma install')} ${dimText('Install ChromaDB')}`)
372
+ console.log(` ${dimText('claude-brain chroma start')} ${dimText('Start the server')}`)
373
+ console.log(` ${dimText('claude-brain chroma status')} ${dimText('Check if running')}`)
374
+ console.log(` ${dimText('claude-brain chroma stop')} ${dimText('Stop the server')}`)
375
+ console.log()
376
+ }
377
+
378
+ // ── Entry Point ───────────────────────────────────────────
379
+
380
+ export async function runChroma(): Promise<void> {
381
+ const subcommand = process.argv[3] || 'help'
382
+
383
+ switch (subcommand) {
384
+ case 'start':
385
+ await chromaStart()
386
+ break
387
+ case 'stop':
388
+ await chromaStop()
389
+ break
390
+ case 'status':
391
+ await chromaStatus()
392
+ break
393
+ case 'install':
394
+ await chromaInstall()
395
+ break
396
+ case 'help':
397
+ case '--help':
398
+ case '-h':
399
+ printChromaHelp()
400
+ break
401
+ default:
402
+ console.log()
403
+ console.log(errorText(`Unknown subcommand: ${subcommand}`))
404
+ printChromaHelp()
405
+ process.exit(1)
406
+ }
407
+ }
@@ -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.0',
6
+ serverVersion: '0.4.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.0'),
199
+ serverVersion: z.string().regex(/^\d+\.\d+\.\d+$/, 'Version must be semver format').default('0.4.1'),
200
200
 
201
201
  /** Logging level */
202
202
  logLevel: LogLevelSchema.default('info'),
@@ -36,12 +36,12 @@ export class ChromaClientManager {
36
36
  break
37
37
 
38
38
  case 'client-server':
39
- if (!this.config.host || !this.config.port) {
40
- throw new Error('Host and port required for client-server mode')
39
+ if (!this.config.host) {
40
+ throw new Error('Host required for client-server mode')
41
41
  }
42
42
  this.client = new ChromaClient({
43
43
  host: this.config.host,
44
- port: this.config.port,
44
+ port: this.config.port || 8000,
45
45
  ...authConfig
46
46
  })
47
47
  break
@@ -45,6 +45,10 @@ export function getChromaConfigFromEnv(): Partial<ChromaConfig> {
45
45
  config.mode = process.env.CHROMA_MODE as any
46
46
  }
47
47
 
48
+ if (process.env.CHROMA_PORT) {
49
+ config.port = parseInt(process.env.CHROMA_PORT, 10)
50
+ }
51
+
48
52
  if (process.env.CHROMA_EMBEDDING_PROVIDER) {
49
53
  config.embeddingProvider = process.env.CHROMA_EMBEDDING_PROVIDER as any
50
54
  }
@@ -23,6 +23,16 @@ export async function handleAnalyzeDecisionEvolution(
23
23
  const { topic, project_name, limit } = input
24
24
 
25
25
  const memory = getMemoryService()
26
+
27
+ if (!memory.isChromaDBEnabled()) {
28
+ return ResponseFormatter.text(
29
+ `No decisions found for topic: "${topic}"\n\n` +
30
+ 'Note: ChromaDB is not connected. Decision evolution analysis requires ChromaDB for semantic search. ' +
31
+ 'Decisions stored via SQLite fallback are available through recall_similar. ' +
32
+ 'To enable evolution tracking, start a ChromaDB server or configure persistent mode.'
33
+ )
34
+ }
35
+
26
36
  const tracker = new DecisionEvolutionTracker(
27
37
  logger,
28
38
  memory.chroma.collections,
@@ -35,10 +45,7 @@ export async function handleAnalyzeDecisionEvolution(
35
45
  })
36
46
 
37
47
  if (analysis.totalDecisions === 0) {
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)
48
+ return ResponseFormatter.text(`No decisions found for topic: "${topic}"`)
42
49
  }
43
50
 
44
51
  const parts: string[] = []
@@ -23,6 +23,16 @@ export async function handleDetectTrends(
23
23
  const { project_name, period_days, min_occurrences, limit } = input
24
24
 
25
25
  const memory = getMemoryService()
26
+
27
+ if (!memory.isChromaDBEnabled()) {
28
+ return ResponseFormatter.text(
29
+ 'No decisions found for trend analysis.\n\n' +
30
+ 'Note: ChromaDB is not connected. Trend detection requires ChromaDB for semantic search across decisions. ' +
31
+ 'Decisions stored via SQLite fallback are available through recall_similar and get_patterns. ' +
32
+ 'To enable trend detection, start a ChromaDB server or configure persistent mode.'
33
+ )
34
+ }
35
+
26
36
  const detector = new TrendDetector(logger, memory.chroma.collections)
27
37
 
28
38
  const analysis = await detector.detectTrends({
@@ -33,10 +43,7 @@ export async function handleDetectTrends(
33
43
  })
34
44
 
35
45
  if (analysis.totalDecisionsAnalyzed === 0) {
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)
46
+ return ResponseFormatter.text('No decisions found for trend analysis.')
40
47
  }
41
48
 
42
49
  const parts: string[] = []
@@ -23,6 +23,16 @@ export async function handleFindCrossProjectPatterns(
23
23
  const { min_projects, limit, query } = input
24
24
 
25
25
  const memory = getMemoryService()
26
+
27
+ if (!memory.isChromaDBEnabled()) {
28
+ return ResponseFormatter.text(
29
+ 'No cross-project patterns found.\n\n' +
30
+ 'Note: ChromaDB is not connected. Cross-project pattern analysis requires ChromaDB for semantic search across decisions. ' +
31
+ 'Decisions stored via SQLite fallback are available through recall_similar and get_patterns. ' +
32
+ 'To enable cross-project analysis, start a ChromaDB server or configure persistent mode.'
33
+ )
34
+ }
35
+
26
36
  const generalizer = new PatternGeneralizer(
27
37
  logger,
28
38
  memory.chroma.collections,
@@ -24,6 +24,16 @@ export async function handleGetDecisionTimeline(
24
24
  const { project_name, topic, time_range, limit } = input
25
25
 
26
26
  const memory = getMemoryService()
27
+
28
+ if (!memory.isChromaDBEnabled()) {
29
+ return ResponseFormatter.text(
30
+ 'No decisions found for the specified criteria.\n\n' +
31
+ 'Note: ChromaDB is not connected. Decision timeline requires ChromaDB for semantic search. ' +
32
+ 'Decisions stored via SQLite fallback are available through recall_similar. ' +
33
+ 'To enable timeline view, start a ChromaDB server or configure persistent mode.'
34
+ )
35
+ }
36
+
27
37
  const timelineBuilder = new TimelineBuilder(
28
38
  logger,
29
39
  memory.chroma.collections,
@@ -50,10 +60,7 @@ export async function handleGetDecisionTimeline(
50
60
  })
51
61
 
52
62
  if (timeline.entries.length === 0) {
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)
63
+ return ResponseFormatter.text('No decisions found for the specified criteria.')
57
64
  }
58
65
 
59
66
  // Format timeline
@@ -23,6 +23,16 @@ export async function handleGetRecommendations(
23
23
  const { query, project_name, limit } = input
24
24
 
25
25
  const memory = getMemoryService()
26
+
27
+ if (!memory.isChromaDBEnabled()) {
28
+ return ResponseFormatter.text(
29
+ `No recommendations found for: "${query}"\n\n` +
30
+ 'Note: ChromaDB is not connected. Recommendations require ChromaDB for semantic search across patterns, corrections, and decisions. ' +
31
+ 'Decisions stored via SQLite fallback are available through recall_similar, get_patterns, and get_corrections. ' +
32
+ 'To enable recommendations, start a ChromaDB server or configure persistent mode.'
33
+ )
34
+ }
35
+
26
36
  const recommender = new Recommender(
27
37
  logger,
28
38
  memory.chroma.collections,
@@ -35,10 +45,7 @@ export async function handleGetRecommendations(
35
45
  })
36
46
 
37
47
  if (result.recommendations.length === 0) {
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)
48
+ return ResponseFormatter.text(`No recommendations found for: "${query}"`)
42
49
  }
43
50
 
44
51
  const parts: string[] = []
@@ -5,7 +5,7 @@
5
5
 
6
6
  import type { Logger } from 'pino'
7
7
  import type { ToolResponse } from '@/tools/types'
8
- import { getKnowledgeGraphService, isServicesInitialized } from '@/server/services'
8
+ import { getKnowledgeGraphService, getMemoryService, isServicesInitialized } from '@/server/services'
9
9
  import { ToolValidator } from '@/server/utils/validators'
10
10
  import { ResponseFormatter } from '@/server/utils/response-formatter'
11
11
  import { ErrorHandler } from '@/server/utils/error-handler'
@@ -72,6 +72,19 @@ export async function handleSearchKnowledgeGraph(
72
72
  edgeCount: result.edges.length
73
73
  }, 'Knowledge graph search complete')
74
74
 
75
+ // If graph is empty and ChromaDB is not connected, add guidance
76
+ if (result.nodes.length === 0 && result.edges.length === 0) {
77
+ const memory = getMemoryService()
78
+ if (!memory.isChromaDBEnabled()) {
79
+ return ResponseFormatter.text(
80
+ 'Knowledge graph is empty (0 nodes, 0 edges).\n\n' +
81
+ 'Note: ChromaDB is not connected. The knowledge graph is populated from stored decisions. ' +
82
+ 'New decisions stored via SQLite will populate the graph going forward. ' +
83
+ 'To migrate existing decisions into the graph, start a ChromaDB server or configure persistent mode.'
84
+ )
85
+ }
86
+ }
87
+
75
88
  return ResponseFormatter.json(response, 'Knowledge Graph Search Results')
76
89
 
77
90
  } catch (error) {
@@ -23,6 +23,17 @@ export async function handleWhatIfAnalysis(
23
23
  const { change, project_name, max_results } = input
24
24
 
25
25
  const memory = getMemoryService()
26
+
27
+ if (!memory.isChromaDBEnabled()) {
28
+ return ResponseFormatter.text(
29
+ `## What-If: "${change}"\n\n` +
30
+ 'Unable to perform what-if analysis.\n\n' +
31
+ 'Note: ChromaDB is not connected. What-if analysis requires ChromaDB for semantic search across decisions. ' +
32
+ 'Decisions stored via SQLite fallback are available through recall_similar. ' +
33
+ 'To enable what-if analysis, start a ChromaDB server or configure persistent mode.'
34
+ )
35
+ }
36
+
26
37
  const kgService = getKnowledgeGraphService()
27
38
  const graph = kgService?.graph || null
28
39
 
@@ -63,10 +74,7 @@ export async function handleWhatIfAnalysis(
63
74
  return ResponseFormatter.text(withMemoryIndicator(content, totalAffected))
64
75
  }
65
76
 
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)
77
+ return ResponseFormatter.text(content)
70
78
 
71
79
  } catch (error) {
72
80
  ErrorHandler.logError(logger, error, { tool: 'what_if_analysis' })
@@ -112,9 +112,9 @@ export async function initializeServices(config: Config, logger: Logger): Promis
112
112
  await phase12.initialize()
113
113
  serviceLogger.info('Phase 12 service initialized')
114
114
 
115
- // Initialize Retrieval Service (Phase 13)
115
+ // Initialize Retrieval Service (Phase 13) — requires ChromaDB
116
116
  let retrieval: RetrievalService | null = null
117
- if (config.retrieval?.feedback?.enabled || config.retrieval?.enabled) {
117
+ if ((config.retrieval?.feedback?.enabled || config.retrieval?.enabled) && memory.isChromaDBEnabled()) {
118
118
  retrieval = new RetrievalService(
119
119
  logger,
120
120
  memory.chroma.collections,
@@ -123,6 +123,8 @@ export async function initializeServices(config: Config, logger: Logger): Promis
123
123
  )
124
124
  await retrieval.initialize()
125
125
  serviceLogger.info('Retrieval service initialized')
126
+ } else if (config.retrieval?.enabled && !memory.isChromaDBEnabled()) {
127
+ serviceLogger.warn('Retrieval service requires ChromaDB, skipping initialization')
126
128
  }
127
129
 
128
130
  // Initialize Knowledge Graph Service (Phase 14)
@@ -146,14 +148,16 @@ export async function initializeServices(config: Config, logger: Logger): Promis
146
148
 
147
149
  knowledgeGraph = { graph, search, builder, linker }
148
150
 
149
- // Migrate existing decisions if graph is empty
150
- if (graph.getNodeCount() === 0) {
151
+ // Migrate existing decisions if graph is empty (requires ChromaDB)
152
+ if (graph.getNodeCount() === 0 && memory.isChromaDBEnabled()) {
151
153
  serviceLogger.info('Empty graph detected, migrating existing decisions...')
152
154
  const migrationResult = await builder.migrateExistingDecisions(memory.chroma.collections)
153
155
  serviceLogger.info(
154
156
  { processed: migrationResult.processed, errors: migrationResult.errors },
155
157
  'Initial graph migration complete'
156
158
  )
159
+ } else if (graph.getNodeCount() === 0) {
160
+ serviceLogger.info('Empty graph detected, but ChromaDB unavailable — graph will populate as new decisions are stored')
157
161
  }
158
162
 
159
163
  // Hook builder into decision storage for real-time graph population
@@ -171,9 +175,9 @@ export async function initializeServices(config: Config, logger: Logger): Promis
171
175
  }
172
176
  }
173
177
 
174
- // Initialize Episode Manager (Phase 14)
178
+ // Initialize Episode Manager (Phase 14) — requires ChromaDB
175
179
  let episodeManager: EpisodeManager | null = null
176
- if (config.knowledge?.episodic?.enabled !== false) {
180
+ if (config.knowledge?.episodic?.enabled !== false && memory.isChromaDBEnabled()) {
177
181
  try {
178
182
  episodeManager = new EpisodeManager(
179
183
  logger,
@@ -187,10 +191,10 @@ export async function initializeServices(config: Config, logger: Logger): Promis
187
191
  }
188
192
  }
189
193
 
190
- // Initialize Semantic Cache & Precompute (Phase 15)
194
+ // Initialize Semantic Cache & Precompute (Phase 15) — requires ChromaDB
191
195
  let semanticCache: SemanticCache | null = null
192
196
  let precompute: PrecomputeEngine | null = null
193
- if (config.advancedIntelligence?.enabled !== false && config.advancedIntelligence?.cache?.enabled !== false) {
197
+ if (config.advancedIntelligence?.enabled !== false && config.advancedIntelligence?.cache?.enabled !== false && memory.isChromaDBEnabled()) {
194
198
  try {
195
199
  const cacheConfig = config.advancedIntelligence?.cache || {}
196
200
  semanticCache = new SemanticCache(logger, {
@@ -1,6 +1,7 @@
1
1
  import prompts from 'prompts'
2
2
  import fs from 'fs/promises'
3
3
  import { existsSync } from 'fs'
4
+ import { execSync } from 'child_process'
4
5
  import path from 'path'
5
6
  import os from 'os'
6
7
  import { fileURLToPath } from 'url'
@@ -21,6 +22,7 @@ export interface SetupAnswers {
21
22
  vaultPath: string
22
23
  logLevel: string
23
24
  enableFileWatch: boolean
25
+ enableChromaDB: boolean
24
26
  createSampleProject: boolean
25
27
  installClaudeMd: boolean
26
28
  }
@@ -34,7 +36,7 @@ export class SetupWizard {
34
36
 
35
37
  async run(): Promise<SetupAnswers> {
36
38
  // Step 1: Detect Vaults
37
- console.log(stepIndicator(1, 5, 'Detecting Obsidian Vaults'))
39
+ console.log(stepIndicator(1, 6, 'Detecting Obsidian Vaults'))
38
40
  await transition()
39
41
 
40
42
  const suggestedPaths = await withSpinner('Scanning for Obsidian vaults', () =>
@@ -49,7 +51,7 @@ export class SetupWizard {
49
51
  console.log()
50
52
 
51
53
  // Step 2: Vault Configuration
52
- console.log(stepIndicator(2, 5, 'Vault Configuration'))
54
+ console.log(stepIndicator(2, 6, 'Vault Configuration'))
53
55
  await transition()
54
56
 
55
57
  const vaultAnswers = await prompts([
@@ -86,7 +88,7 @@ export class SetupWizard {
86
88
  const finalVaultPath = vaultAnswers.vaultPath || vaultAnswers.vaultPathChoice
87
89
 
88
90
  // Step 3: Logging
89
- console.log(stepIndicator(3, 5, 'Logging Configuration'))
91
+ console.log(stepIndicator(3, 6, 'Logging Configuration'))
90
92
  await transition()
91
93
 
92
94
  const loggingAnswers = await prompts({
@@ -103,7 +105,7 @@ export class SetupWizard {
103
105
  })
104
106
 
105
107
  // Step 4: Features
106
- console.log(stepIndicator(4, 5, 'Feature Selection'))
108
+ console.log(stepIndicator(4, 6, 'Feature Selection'))
107
109
  await transition()
108
110
 
109
111
  const featureAnswers = await prompts([
@@ -127,14 +129,40 @@ export class SetupWizard {
127
129
  }
128
130
  ])
129
131
 
130
- // Step 5: Review
131
- console.log(stepIndicator(5, 5, 'Review Configuration'))
132
+ // Step 5: ChromaDB
133
+ console.log(stepIndicator(5, 6, 'ChromaDB (Vector Database)'))
134
+ await transition()
135
+
136
+ console.log(dimText('ChromaDB enables semantic search, knowledge graph,'))
137
+ console.log(dimText('and advanced intelligence features. Requires Python 3.'))
138
+ console.log()
139
+
140
+ const chromaInstalled = this.isChromaInstalled()
141
+ if (chromaInstalled) {
142
+ console.log(successText('ChromaDB CLI detected'))
143
+ } else {
144
+ console.log(warningText('ChromaDB CLI not found — you can install it later'))
145
+ }
146
+ console.log()
147
+
148
+ const chromaAnswers = await prompts({
149
+ type: 'confirm',
150
+ name: 'enableChromaDB',
151
+ message: chromaInstalled
152
+ ? 'Enable ChromaDB for advanced features?'
153
+ : 'Enable ChromaDB? (install later: pip install chromadb)',
154
+ initial: chromaInstalled
155
+ })
156
+
157
+ // Step 6: Review
158
+ console.log(stepIndicator(6, 6, 'Review Configuration'))
132
159
  await transition()
133
160
 
134
161
  const answers: SetupAnswers = {
135
162
  vaultPath: finalVaultPath,
136
163
  logLevel: loggingAnswers.logLevel ?? 'warn',
137
164
  enableFileWatch: featureAnswers.enableFileWatch ?? true,
165
+ enableChromaDB: chromaAnswers.enableChromaDB ?? false,
138
166
  createSampleProject: featureAnswers.createSampleProject ?? true,
139
167
  installClaudeMd: featureAnswers.installClaudeMd ?? true,
140
168
  }
@@ -143,6 +171,7 @@ export class SetupWizard {
143
171
  { label: 'Vault Path', value: answers.vaultPath, status: 'success' },
144
172
  { label: 'Log Level', value: answers.logLevel, status: 'info' },
145
173
  { label: 'File Watching', value: answers.enableFileWatch ? 'Enabled' : 'Disabled', status: answers.enableFileWatch ? 'success' : 'warning' },
174
+ { label: 'ChromaDB', value: answers.enableChromaDB ? 'Enabled' : 'Disabled', status: answers.enableChromaDB ? 'success' : 'warning' },
146
175
  { label: 'Sample Project', value: answers.createSampleProject ? 'Yes' : 'No', status: 'info' },
147
176
  { label: 'Install CLAUDE.md', value: answers.installClaudeMd ? 'Yes' : 'No', status: 'info' },
148
177
  ]))
@@ -191,11 +220,31 @@ export class SetupWizard {
191
220
  return locations
192
221
  }
193
222
 
223
+ private isChromaInstalled(): boolean {
224
+ try {
225
+ execSync('chroma --version', { stdio: 'pipe', timeout: 5000 })
226
+ return true
227
+ } catch {
228
+ return false
229
+ }
230
+ }
231
+
194
232
  async applyConfiguration(answers: SetupAnswers): Promise<void> {
195
233
  console.log('\n' + heading('Applying configuration...') + '\n')
196
234
 
197
235
  const homePaths = getHomePaths()
198
236
 
237
+ const chromaEnvLines = answers.enableChromaDB
238
+ ? [
239
+ '',
240
+ '# ChromaDB Configuration',
241
+ 'CHROMA_MODE=client-server',
242
+ 'CHROMA_HOST=localhost',
243
+ 'CHROMA_PORT=8000',
244
+ 'CHROMA_EMBEDDING_PROVIDER=transformers',
245
+ ].join('\n')
246
+ : ''
247
+
199
248
  const envContent = `# Claude Brain Configuration
200
249
  VAULT_PATH=${answers.vaultPath}
201
250
  LOG_LEVEL=${answers.logLevel}
@@ -203,6 +252,7 @@ ENABLE_FILE_WATCH=${answers.enableFileWatch}
203
252
  DB_PATH=./data/memory.db
204
253
  LOG_FILE_PATH=./logs/claude-brain.log
205
254
  SERVER_NAME=claude-brain
255
+ ${chromaEnvLines}
206
256
  `
207
257
 
208
258
  await withSpinner('Writing .env configuration', async () => {
@@ -211,6 +261,7 @@ SERVER_NAME=claude-brain
211
261
 
212
262
  await withSpinner('Creating data and log directories', async () => {
213
263
  await fs.mkdir(homePaths.data, { recursive: true })
264
+ await fs.mkdir(homePaths.chroma, { recursive: true })
214
265
  await fs.mkdir(homePaths.logs, { recursive: true })
215
266
  })
216
267
 
@@ -229,16 +280,35 @@ SERVER_NAME=claude-brain
229
280
  }
230
281
  }
231
282
 
232
- console.log()
233
- console.log(box([
283
+ const nextSteps: string[] = [
234
284
  heading('Setup complete!'),
235
285
  '',
236
286
  dimText('Next steps:'),
237
287
  ` ${theme.primary('1.')} ${theme.bold('claude-brain install')}`,
238
288
  ` ${dimText('Register as MCP server')}`,
239
- ` ${theme.primary('2.')} ${theme.bold('claude-brain health')}`,
240
- ` ${dimText('Verify everything works')}`,
241
- ].join('\n'), 'Done'))
289
+ ]
290
+
291
+ if (answers.enableChromaDB) {
292
+ nextSteps.push(
293
+ ` ${theme.primary('2.')} ${theme.bold('claude-brain chroma install')}`,
294
+ ` ${dimText('Install ChromaDB (if not already installed)')}`,
295
+ ` ${theme.primary('3.')} ${theme.bold('claude-brain chroma start')}`,
296
+ ` ${dimText('Start the ChromaDB server')}`,
297
+ ` ${theme.primary('4.')} ${theme.bold('claude-brain health')}`,
298
+ ` ${dimText('Verify everything works')}`,
299
+ )
300
+ } else {
301
+ nextSteps.push(
302
+ ` ${theme.primary('2.')} ${theme.bold('claude-brain health')}`,
303
+ ` ${dimText('Verify everything works')}`,
304
+ '',
305
+ dimText('Optional: Enable ChromaDB later for advanced features:'),
306
+ ` ${dimText('claude-brain chroma install && claude-brain chroma start')}`,
307
+ )
308
+ }
309
+
310
+ console.log()
311
+ console.log(box(nextSteps.join('\n'), 'Done'))
242
312
  console.log()
243
313
  }
244
314