claude-brain 0.3.7 → 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.3.7
1
+ 0.4.1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-brain",
3
- "version": "0.3.7",
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.3.7',
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.3.7'),
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'),
@@ -248,6 +248,26 @@ export class StandardsManager {
248
248
  'Ignoring error handling',
249
249
  'Deep nesting'
250
250
  ]
251
+ },
252
+ {
253
+ language: 'JavaScript',
254
+ version: 'ES2022+',
255
+ style: 'standard',
256
+ linting: {},
257
+ conventions: [
258
+ 'Use strict mode',
259
+ 'Prefer const over let',
260
+ 'Use async/await over promises',
261
+ 'Use optional chaining and nullish coalescing',
262
+ 'Add JSDoc comments for public APIs'
263
+ ],
264
+ antiPatterns: [
265
+ 'Using var',
266
+ 'Callback hell',
267
+ 'Ignoring error handling',
268
+ 'Deep nesting',
269
+ 'Mutating function arguments'
270
+ ]
251
271
  }
252
272
  ],
253
273
  frameworks: [],
@@ -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
@@ -82,8 +82,9 @@ export class ChromaClientManager {
82
82
  this.isConnected = true
83
83
  this.logger.info('ChromaDB client initialized successfully')
84
84
  } catch (heartbeatError) {
85
- this.logger.warn({ error: heartbeatError }, 'ChromaDB heartbeat failed, connection may not be fully established')
86
- this.isConnected = true
85
+ this.isConnected = false
86
+ this.logger.warn({ error: heartbeatError }, 'ChromaDB heartbeat failed')
87
+ throw new Error(`ChromaDB server unreachable: ${heartbeatError instanceof Error ? heartbeatError.message : String(heartbeatError)}`)
87
88
  }
88
89
 
89
90
  } catch (error) {
@@ -35,7 +35,18 @@ export function getChromaConfigFromEnv(): Partial<ChromaConfig> {
35
35
 
36
36
  if (process.env.CHROMA_HOST) {
37
37
  config.host = process.env.CHROMA_HOST
38
- config.mode = 'cloud'
38
+ // Only default to cloud if CHROMA_MODE isn't explicitly set
39
+ if (!process.env.CHROMA_MODE) {
40
+ config.mode = 'cloud'
41
+ }
42
+ }
43
+
44
+ if (process.env.CHROMA_MODE) {
45
+ config.mode = process.env.CHROMA_MODE as any
46
+ }
47
+
48
+ if (process.env.CHROMA_PORT) {
49
+ config.port = parseInt(process.env.CHROMA_PORT, 10)
39
50
  }
40
51
 
41
52
  if (process.env.CHROMA_EMBEDDING_PROVIDER) {