claude-brain 0.30.2 → 0.30.3

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.
Files changed (236) hide show
  1. package/README.md +241 -191
  2. package/VERSION +1 -1
  3. package/assets/CLAUDE-unified.md +11 -11
  4. package/assets/CLAUDE.md +29 -29
  5. package/package.json +7 -3
  6. package/packs/backend/node.json +173 -173
  7. package/packs/core/javascript.json +176 -176
  8. package/packs/core/typescript.json +222 -222
  9. package/packs/frontend/react.json +254 -254
  10. package/packs/meta/testing.json +172 -172
  11. package/scripts/postinstall.mjs +531 -531
  12. package/src/automation/decision-detector.ts +452 -452
  13. package/src/automation/phase12-manager.ts +456 -456
  14. package/src/automation/proactive-recall.ts +373 -373
  15. package/src/automation/project-detector.ts +310 -310
  16. package/src/automation/repo-scanner.ts +210 -205
  17. package/src/cli/auto-setup.ts +75 -75
  18. package/src/cli/auto-start.ts +266 -266
  19. package/src/cli/bin.ts +264 -264
  20. package/src/cli/commands/autostart.ts +90 -90
  21. package/src/cli/commands/chroma.ts +578 -577
  22. package/src/cli/commands/export-training.ts +70 -70
  23. package/src/cli/commands/export.ts +130 -130
  24. package/src/cli/commands/git-hook.ts +183 -183
  25. package/src/cli/commands/hooks.ts +217 -217
  26. package/src/cli/commands/init.ts +123 -123
  27. package/src/cli/commands/install-mcp.ts +122 -111
  28. package/src/cli/commands/models.ts +979 -979
  29. package/src/cli/commands/pack.ts +200 -200
  30. package/src/cli/commands/refresh.ts +344 -339
  31. package/src/cli/commands/reindex.ts +120 -120
  32. package/src/cli/commands/serve.ts +466 -463
  33. package/src/cli/commands/start.ts +44 -44
  34. package/src/cli/commands/status.ts +220 -203
  35. package/src/cli/commands/uninstall-mcp.ts +45 -41
  36. package/src/cli/commands/update.ts +130 -124
  37. package/src/cli/migrate-chroma.ts +106 -106
  38. package/src/cli/ui/animations.ts +80 -80
  39. package/src/cli/ui/components.ts +82 -82
  40. package/src/cli/ui/index.ts +4 -4
  41. package/src/cli/ui/logo.ts +36 -36
  42. package/src/cli/ui/theme.ts +55 -55
  43. package/src/code-intelligence/indexer.ts +352 -352
  44. package/src/code-intelligence/linker.ts +178 -178
  45. package/src/code-intelligence/parser.ts +484 -484
  46. package/src/code-intelligence/query.ts +291 -291
  47. package/src/code-intelligence/schema.ts +83 -83
  48. package/src/code-intelligence/types.ts +95 -95
  49. package/src/config/defaults.ts +52 -52
  50. package/src/config/home.ts +56 -56
  51. package/src/config/index.ts +5 -5
  52. package/src/config/loader.ts +192 -192
  53. package/src/config/schema.ts +446 -415
  54. package/src/config/validator.ts +182 -182
  55. package/src/context/assembler.ts +407 -400
  56. package/src/context/index.ts +79 -79
  57. package/src/context/progress-tracker.ts +174 -174
  58. package/src/context/standards-manager.ts +287 -287
  59. package/src/context/validator.ts +58 -58
  60. package/src/diagnostics/index.ts +122 -121
  61. package/src/health/index.ts +233 -232
  62. package/src/hooks/brain-hook.ts +134 -131
  63. package/src/hooks/capture.ts +168 -168
  64. package/src/hooks/claude-code-mastery.md +112 -112
  65. package/src/hooks/context-hook.ts +260 -245
  66. package/src/hooks/deduplicator.ts +72 -72
  67. package/src/hooks/git-capture.ts +109 -109
  68. package/src/hooks/git-hook-installer.ts +211 -207
  69. package/src/hooks/index.ts +20 -20
  70. package/src/hooks/installer.ts +306 -288
  71. package/src/hooks/interceptor-hook.ts +204 -201
  72. package/src/hooks/passive-classifier.ts +397 -397
  73. package/src/hooks/queue.ts +160 -129
  74. package/src/hooks/session-tracker.ts +312 -312
  75. package/src/hooks/types.ts +52 -52
  76. package/src/index.ts +7 -7
  77. package/src/intelligence/cross-project/generalizer.ts +283 -283
  78. package/src/intelligence/cross-project/index.ts +7 -7
  79. package/src/intelligence/hf-downloader.ts +222 -222
  80. package/src/intelligence/hf-manifest.json +78 -78
  81. package/src/intelligence/index.ts +24 -24
  82. package/src/intelligence/inference-router.ts +762 -762
  83. package/src/intelligence/model-manager.ts +263 -245
  84. package/src/intelligence/optimization/index.ts +10 -10
  85. package/src/intelligence/optimization/precompute.ts +202 -202
  86. package/src/intelligence/optimization/semantic-cache.ts +213 -207
  87. package/src/intelligence/prediction/index.ts +7 -7
  88. package/src/intelligence/prediction/recommender.ts +276 -268
  89. package/src/intelligence/reasoning/chain-retrieval.ts +243 -247
  90. package/src/intelligence/reasoning/index.ts +7 -7
  91. package/src/intelligence/temporal/evolution.ts +193 -197
  92. package/src/intelligence/temporal/index.ts +16 -16
  93. package/src/intelligence/temporal/query-processor.ts +190 -190
  94. package/src/intelligence/temporal/timeline.ts +272 -259
  95. package/src/intelligence/temporal/trends.ts +263 -263
  96. package/src/intelligence/tokenizer.ts +118 -118
  97. package/src/knowledge/entity-extractor.ts +447 -443
  98. package/src/knowledge/graph/builder.ts +185 -185
  99. package/src/knowledge/graph/linker.ts +201 -201
  100. package/src/knowledge/graph/memory-graph.ts +359 -359
  101. package/src/knowledge/graph/schema.ts +99 -99
  102. package/src/knowledge/graph/search.ts +166 -166
  103. package/src/knowledge/relationship-extractor.ts +108 -108
  104. package/src/memory/chroma/client.ts +211 -192
  105. package/src/memory/chroma/collection-manager.ts +92 -92
  106. package/src/memory/chroma/config.ts +57 -57
  107. package/src/memory/chroma/embeddings.ts +177 -175
  108. package/src/memory/chroma/index.ts +82 -82
  109. package/src/memory/chroma/migration.ts +270 -270
  110. package/src/memory/chroma/schemas.ts +69 -69
  111. package/src/memory/chroma/search.ts +319 -315
  112. package/src/memory/chroma/store.ts +755 -747
  113. package/src/memory/compression.ts +121 -121
  114. package/src/memory/consolidation/archiver.ts +162 -165
  115. package/src/memory/consolidation/merger.ts +182 -186
  116. package/src/memory/consolidation/scorer.ts +136 -136
  117. package/src/memory/database.ts +9 -0
  118. package/src/memory/dual-write.ts +145 -0
  119. package/src/memory/embeddings.ts +226 -226
  120. package/src/memory/episodic/detector.ts +108 -108
  121. package/src/memory/episodic/manager.ts +347 -351
  122. package/src/memory/episodic/summarizer.ts +179 -179
  123. package/src/memory/episodic/types.ts +52 -52
  124. package/src/memory/fts5-search.ts +692 -633
  125. package/src/memory/index.ts +943 -1060
  126. package/src/memory/migrations/add-fts5.ts +118 -108
  127. package/src/memory/patterns.ts +438 -438
  128. package/src/memory/pruning.ts +60 -60
  129. package/src/memory/schema.ts +88 -88
  130. package/src/memory/store.ts +911 -787
  131. package/src/orchestrator/handlers/decision-handler.ts +204 -204
  132. package/src/packs/index.ts +9 -9
  133. package/src/packs/loader.ts +134 -134
  134. package/src/packs/manager.ts +204 -204
  135. package/src/packs/ranker.ts +78 -78
  136. package/src/packs/types.ts +81 -81
  137. package/src/phase12/index.ts +5 -5
  138. package/src/retrieval/bm25/index.ts +300 -297
  139. package/src/retrieval/bm25/tokenizer.ts +184 -184
  140. package/src/retrieval/feedback/adaptive.ts +221 -221
  141. package/src/retrieval/feedback/index.ts +16 -16
  142. package/src/retrieval/feedback/metrics.ts +221 -221
  143. package/src/retrieval/feedback/store.ts +283 -283
  144. package/src/retrieval/fusion/index.ts +194 -194
  145. package/src/retrieval/fusion/rrf.ts +165 -165
  146. package/src/retrieval/index.ts +12 -12
  147. package/src/retrieval/pipeline.ts +375 -375
  148. package/src/retrieval/query/expander.ts +203 -203
  149. package/src/retrieval/query/index.ts +27 -27
  150. package/src/retrieval/query/intent-classifier.ts +252 -252
  151. package/src/retrieval/query/temporal-parser.ts +295 -295
  152. package/src/retrieval/reranker/index.ts +189 -188
  153. package/src/retrieval/reranker/model.ts +99 -95
  154. package/src/retrieval/service.ts +125 -125
  155. package/src/retrieval/types.ts +162 -162
  156. package/src/routing/entity-extractor.ts +454 -454
  157. package/src/routing/handlers/exploration-handler.ts +369 -0
  158. package/src/routing/handlers/index.ts +19 -0
  159. package/src/routing/handlers/memory-handler.ts +273 -0
  160. package/src/routing/handlers/mutation-handler.ts +241 -0
  161. package/src/routing/handlers/recall-handler.ts +642 -0
  162. package/src/routing/handlers/shared.ts +515 -0
  163. package/src/routing/handlers/types.ts +48 -0
  164. package/src/routing/intent-classifier.ts +552 -552
  165. package/src/routing/response-filter.ts +399 -391
  166. package/src/routing/router.ts +245 -2193
  167. package/src/routing/search-engine.ts +521 -514
  168. package/src/routing/types.ts +104 -94
  169. package/src/scripts/health-check.ts +118 -118
  170. package/src/scripts/setup.ts +122 -122
  171. package/src/server/auto-updater.ts +283 -276
  172. package/src/server/handlers/call-tool.ts +159 -159
  173. package/src/server/handlers/list-tools.ts +35 -35
  174. package/src/server/handlers/tools/auto-remember.ts +165 -165
  175. package/src/server/handlers/tools/brain.ts +86 -86
  176. package/src/server/handlers/tools/create-project.ts +135 -135
  177. package/src/server/handlers/tools/get-code-standards.ts +123 -123
  178. package/src/server/handlers/tools/get-corrections.ts +152 -152
  179. package/src/server/handlers/tools/get-patterns.ts +156 -156
  180. package/src/server/handlers/tools/get-project-context.ts +75 -75
  181. package/src/server/handlers/tools/index.ts +30 -30
  182. package/src/server/handlers/tools/init-project.ts +756 -756
  183. package/src/server/handlers/tools/list-projects.ts +126 -126
  184. package/src/server/handlers/tools/recall-similar.ts +87 -87
  185. package/src/server/handlers/tools/recognize-pattern.ts +132 -132
  186. package/src/server/handlers/tools/record-correction.ts +131 -131
  187. package/src/server/handlers/tools/remember-decision.ts +168 -168
  188. package/src/server/handlers/tools/schemas.ts +179 -179
  189. package/src/server/handlers/tools/search-code.ts +122 -122
  190. package/src/server/handlers/tools/smart-context.ts +146 -146
  191. package/src/server/handlers/tools/update-progress.ts +131 -131
  192. package/src/server/http-api.ts +215 -1229
  193. package/src/server/mcp-proxy.ts +85 -84
  194. package/src/server/mcp-server.ts +285 -284
  195. package/src/server/middleware/auth.ts +39 -0
  196. package/src/server/middleware/error-handler.ts +37 -0
  197. package/src/server/middleware/rate-limit.ts +53 -0
  198. package/src/server/middleware/validate.ts +42 -0
  199. package/src/server/pid-manager.ts +137 -136
  200. package/src/server/providers/resources.ts +581 -581
  201. package/src/server/routes/code.ts +228 -0
  202. package/src/server/routes/context.ts +26 -0
  203. package/src/server/routes/health.ts +19 -0
  204. package/src/server/routes/helpers.ts +100 -0
  205. package/src/server/routes/hooks.ts +197 -0
  206. package/src/server/routes/mcp.ts +47 -0
  207. package/src/server/routes/memory.ts +397 -0
  208. package/src/server/routes/models.ts +96 -0
  209. package/src/server/routes/projects.ts +89 -0
  210. package/src/server/routes/types.ts +21 -0
  211. package/src/server/schemas/api-schemas.ts +202 -0
  212. package/src/server/services.ts +720 -720
  213. package/src/server/utils/memory-indicator.ts +84 -84
  214. package/src/server/utils/response-formatter.ts +129 -129
  215. package/src/server/web-viewer.ts +1145 -1115
  216. package/src/setup/index.ts +38 -38
  217. package/src/tools/registry.ts +115 -115
  218. package/src/tools/schemas.ts +666 -666
  219. package/src/tools/types.ts +412 -412
  220. package/src/training/data-store.ts +320 -298
  221. package/src/training/retrain-pipeline.ts +399 -394
  222. package/src/utils/error-handler.ts +136 -136
  223. package/src/utils/index.ts +58 -58
  224. package/src/utils/kill-port.ts +55 -53
  225. package/src/utils/phase12-helper.ts +56 -56
  226. package/src/utils/safe-path.ts +43 -0
  227. package/src/utils/timing.ts +47 -47
  228. package/src/utils/transaction.ts +63 -63
  229. package/src/vault/index.ts +4 -3
  230. package/src/vault/paths.ts +106 -106
  231. package/src/vault/query.ts +4 -1
  232. package/src/vault/reader.ts +44 -1
  233. package/src/vault/watcher.ts +24 -1
  234. package/src/vault/writer.ts +487 -413
  235. package/skills/persistent-memory/SKILL.md +0 -148
  236. package/skills/persistent-memory/references/tool-reference.md +0 -90
@@ -1,577 +1,578 @@
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 { parseArgs } from 'citty'
10
- import { getHomePaths } from '@/config/home'
11
- import {
12
- theme, heading, successText, errorText, warningText, dimText,
13
- infoPanel,
14
- } from '@/cli/ui/index.js'
15
-
16
- const PID_FILENAME = 'chroma.pid'
17
- const DEFAULT_PORT = '8000'
18
-
19
- function getPidFilePath(): string {
20
- const paths = getHomePaths()
21
- return join(paths.data, PID_FILENAME)
22
- }
23
-
24
- function getChromaDataPath(): string {
25
- const paths = getHomePaths()
26
- return paths.chroma
27
- }
28
-
29
- /**
30
- * Find the chroma binary - checks PATH first, then common pip install locations
31
- */
32
- function findChromaBinary(): string | null {
33
- const isWindows = process.platform === 'win32'
34
- const chromaName = isWindows ? 'chroma.exe' : 'chroma'
35
-
36
- // Try bare 'chroma' first (on PATH)
37
- try {
38
- execSync('chroma --version', { stdio: 'pipe', timeout: 5000 })
39
- return 'chroma'
40
- } catch {}
41
-
42
- const { homedir } = require('os')
43
- const home = homedir()
44
-
45
- // Platform-specific search paths
46
- const candidates: string[] = isWindows
47
- ? [
48
- // Windows pip install locations
49
- join(home, 'AppData', 'Local', 'Programs', 'Python', 'Python39', 'Scripts', chromaName),
50
- join(home, 'AppData', 'Local', 'Programs', 'Python', 'Python310', 'Scripts', chromaName),
51
- join(home, 'AppData', 'Local', 'Programs', 'Python', 'Python311', 'Scripts', chromaName),
52
- join(home, 'AppData', 'Local', 'Programs', 'Python', 'Python312', 'Scripts', chromaName),
53
- join(home, 'AppData', 'Local', 'Programs', 'Python', 'Python313', 'Scripts', chromaName),
54
- // Windows user pip install (pip install --user)
55
- join(home, 'AppData', 'Roaming', 'Python', 'Python39', 'Scripts', chromaName),
56
- join(home, 'AppData', 'Roaming', 'Python', 'Python310', 'Scripts', chromaName),
57
- join(home, 'AppData', 'Roaming', 'Python', 'Python311', 'Scripts', chromaName),
58
- join(home, 'AppData', 'Roaming', 'Python', 'Python312', 'Scripts', chromaName),
59
- join(home, 'AppData', 'Roaming', 'Python', 'Python313', 'Scripts', chromaName),
60
- // Scoop / Chocolatey / winget
61
- join(home, 'scoop', 'shims', chromaName),
62
- 'C:\\Python39\\Scripts\\' + chromaName,
63
- 'C:\\Python310\\Scripts\\' + chromaName,
64
- 'C:\\Python311\\Scripts\\' + chromaName,
65
- 'C:\\Python312\\Scripts\\' + chromaName,
66
- 'C:\\Python313\\Scripts\\' + chromaName,
67
- ]
68
- : [
69
- // macOS pip install locations
70
- join(home, 'Library', 'Python', '3.9', 'bin', chromaName),
71
- join(home, 'Library', 'Python', '3.10', 'bin', chromaName),
72
- join(home, 'Library', 'Python', '3.11', 'bin', chromaName),
73
- join(home, 'Library', 'Python', '3.12', 'bin', chromaName),
74
- join(home, 'Library', 'Python', '3.13', 'bin', chromaName),
75
- // Linux pip install locations
76
- join(home, '.local', 'bin', chromaName),
77
- '/usr/local/bin/' + chromaName,
78
- '/opt/homebrew/bin/' + chromaName,
79
- ]
80
-
81
- for (const candidate of candidates) {
82
- try {
83
- if (existsSync(candidate)) {
84
- execSync(`"${candidate}" --version`, { stdio: 'pipe', timeout: 5000 })
85
- return candidate
86
- }
87
- } catch {}
88
- }
89
-
90
- // Try finding via python -m site (works on all platforms)
91
- const pythonCmd = isWindows ? 'python' : 'python3'
92
- try {
93
- const sitePackages = execSync(`${pythonCmd} -c "import site; print(site.getusersitepackages())"`, {
94
- encoding: 'utf-8', stdio: 'pipe', timeout: 5000
95
- }).trim()
96
-
97
- let binDir: string
98
- if (isWindows) {
99
- // Windows: C:\Users\x\AppData\Roaming\Python\Python311\site-packages → Scripts
100
- binDir = sitePackages.replace(/[\\\/]site-packages$/, '\\Scripts')
101
- } else {
102
- // Unix: /Users/x/Library/Python/3.9/lib/python/site-packages → bin
103
- binDir = sitePackages.replace(/\/lib\/.*/, '/bin')
104
- }
105
-
106
- const chromaPath = join(binDir, chromaName)
107
- if (existsSync(chromaPath)) {
108
- execSync(`"${chromaPath}" --version`, { stdio: 'pipe', timeout: 5000 })
109
- return chromaPath
110
- }
111
- } catch {}
112
-
113
- return null
114
- }
115
-
116
- let _cachedChromaBinary: string | null | undefined = undefined
117
-
118
- function getChromaBinary(): string | null {
119
- if (_cachedChromaBinary === undefined) {
120
- _cachedChromaBinary = findChromaBinary()
121
- }
122
- return _cachedChromaBinary
123
- }
124
-
125
- function isChromaCliInstalled(): boolean {
126
- return getChromaBinary() !== null
127
- }
128
-
129
- function getChromaVersion(): string {
130
- const binary = getChromaBinary()
131
- if (!binary) return 'unknown'
132
- try {
133
- return execSync(`"${binary}" --version`, { encoding: 'utf-8', stdio: 'pipe', timeout: 5000 }).trim()
134
- } catch {
135
- return 'unknown'
136
- }
137
- }
138
-
139
- function isPythonInstalled(): { installed: boolean; cmd: string } {
140
- for (const cmd of ['python3', 'python']) {
141
- try {
142
- execSync(`${cmd} --version`, { stdio: 'pipe', timeout: 5000 })
143
- return { installed: true, cmd }
144
- } catch {
145
- // try next
146
- }
147
- }
148
- return { installed: false, cmd: 'python3' }
149
- }
150
-
151
- function getRunningPid(): number | null {
152
- const pidPath = getPidFilePath()
153
- if (!existsSync(pidPath)) return null
154
-
155
- try {
156
- const pid = parseInt(readFileSync(pidPath, 'utf-8').trim(), 10)
157
- if (isNaN(pid)) {
158
- unlinkSync(pidPath)
159
- return null
160
- }
161
- // Signal 0 tests if process exists without killing it
162
- process.kill(pid, 0)
163
- return pid
164
- } catch {
165
- // Process not running, clean up stale PID file
166
- try { unlinkSync(pidPath) } catch {}
167
- return null
168
- }
169
- }
170
-
171
- function isChromaReachable(): boolean {
172
- // Try v2 API first (ChromaDB 1.x server), then v1 (older servers)
173
- for (const apiVersion of ['v2', 'v1']) {
174
- try {
175
- execSync(`curl -sf http://localhost:${DEFAULT_PORT}/api/${apiVersion}/heartbeat`, {
176
- stdio: 'pipe',
177
- timeout: 3000
178
- })
179
- return true
180
- } catch {}
181
- }
182
- return false
183
- }
184
-
185
- async function sleep(ms: number): Promise<void> {
186
- return new Promise(resolve => setTimeout(resolve, ms))
187
- }
188
-
189
- // ── Subcommands ───────────────────────────────────────────
190
-
191
- async function chromaStart(): Promise<void> {
192
- console.log()
193
- console.log(heading('Starting ChromaDB Server'))
194
- console.log()
195
-
196
- // Check if already running
197
- const existingPid = getRunningPid()
198
- if (existingPid) {
199
- console.log(warningText(`ChromaDB is already running (PID: ${existingPid})`))
200
- console.log()
201
- return
202
- }
203
-
204
- if (isChromaReachable()) {
205
- console.log(warningText('A ChromaDB server is already running on port ' + DEFAULT_PORT))
206
- console.log()
207
- return
208
- }
209
-
210
- // Check if chroma CLI is installed
211
- const chromaBinary = getChromaBinary()
212
- if (!chromaBinary) {
213
- console.log(errorText('ChromaDB CLI is not installed or not found on PATH.'))
214
- console.log()
215
- console.log(dimText('Install it with:'))
216
- console.log(` ${theme.primary('pip install chromadb')}`)
217
- console.log()
218
- console.log(dimText('Or run:'))
219
- console.log(` ${theme.primary('claude-brain chroma install')}`)
220
- console.log()
221
- process.exit(1)
222
- }
223
-
224
- const dataPath = getChromaDataPath()
225
-
226
- console.log(dimText(`Binary: ${chromaBinary}`))
227
- console.log(dimText(`Data path: ${dataPath}`))
228
- console.log(dimText(`Port: ${DEFAULT_PORT}`))
229
- console.log()
230
-
231
- // Start ChromaDB server in background
232
- const child = spawn(chromaBinary, ['run', '--path', dataPath, '--port', DEFAULT_PORT], {
233
- detached: true,
234
- stdio: ['ignore', 'pipe', 'pipe'],
235
- env: { ...process.env }
236
- })
237
-
238
- const pid = child.pid
239
- if (!pid) {
240
- console.log(errorText('Failed to start ChromaDB server — no PID returned.'))
241
- console.log()
242
- process.exit(1)
243
- }
244
-
245
- // Detach the child process so it runs independently
246
- child.unref()
247
-
248
- // Save PID file
249
- writeFileSync(getPidFilePath(), String(pid), 'utf-8')
250
-
251
- // Capture early output for error detection
252
- let startupOutput = ''
253
- child.stderr?.on('data', (chunk: Buffer) => {
254
- startupOutput += chunk.toString()
255
- })
256
- child.stdout?.on('data', (chunk: Buffer) => {
257
- startupOutput += chunk.toString()
258
- })
259
-
260
- // Wait up to 15 seconds for server to become reachable
261
- let ready = false
262
- for (let i = 0; i < 30; i++) {
263
- await sleep(500)
264
- if (isChromaReachable()) {
265
- ready = true
266
- break
267
- }
268
- // Check if process exited early
269
- try {
270
- process.kill(pid, 0)
271
- } catch {
272
- break
273
- }
274
- }
275
-
276
- if (ready) {
277
- console.log(successText(`ChromaDB server started (PID: ${pid})`))
278
- console.log()
279
- console.log(dimText('The server is running in the background.'))
280
- console.log(dimText('Stop it with: ') + theme.primary('claude-brain chroma stop'))
281
- } else {
282
- // Check if process is still alive
283
- let alive = false
284
- try {
285
- process.kill(pid, 0)
286
- alive = true
287
- } catch {}
288
-
289
- if (alive) {
290
- console.log(warningText('ChromaDB server started but not yet responding.'))
291
- console.log(dimText(`PID: ${pid} it may still be initializing.`))
292
- console.log(dimText('Check with: ') + theme.primary('claude-brain chroma status'))
293
- } else {
294
- console.log(errorText('ChromaDB server failed to start.'))
295
- if (startupOutput) {
296
- console.log(dimText('Output:'))
297
- console.log(dimText(startupOutput.slice(0, 500)))
298
- }
299
- try { unlinkSync(getPidFilePath()) } catch {}
300
- process.exit(1)
301
- }
302
- }
303
- console.log()
304
- }
305
-
306
- async function chromaStop(): Promise<void> {
307
- console.log()
308
- console.log(heading('Stopping ChromaDB Server'))
309
- console.log()
310
-
311
- const pid = getRunningPid()
312
- if (!pid) {
313
- if (isChromaReachable()) {
314
- console.log(warningText('A ChromaDB server is running on port ' + DEFAULT_PORT + ' but was not started by claude-brain.'))
315
- console.log(dimText('Kill it manually if needed.'))
316
- } else {
317
- console.log(dimText('No ChromaDB server is currently running.'))
318
- }
319
- console.log()
320
- return
321
- }
322
-
323
- try {
324
- process.kill(pid, 'SIGTERM')
325
-
326
- // Wait up to 5 seconds for graceful shutdown
327
- for (let i = 0; i < 10; i++) {
328
- await sleep(500)
329
- try {
330
- process.kill(pid, 0)
331
- } catch {
332
- // Process exited
333
- break
334
- }
335
- }
336
-
337
- // Force kill if still running
338
- try {
339
- process.kill(pid, 0)
340
- // Still alive, force kill
341
- process.kill(pid, 'SIGKILL')
342
- } catch {
343
- // Already dead, good
344
- }
345
-
346
- try { unlinkSync(getPidFilePath()) } catch {}
347
-
348
- console.log(successText(`ChromaDB server stopped (PID: ${pid})`))
349
- } catch (error) {
350
- console.log(errorText(`Failed to stop ChromaDB: ${error instanceof Error ? error.message : String(error)}`))
351
- try { unlinkSync(getPidFilePath()) } catch {}
352
- }
353
- console.log()
354
- }
355
-
356
- async function chromaStatus(): Promise<void> {
357
- console.log()
358
-
359
- const pid = getRunningPid()
360
- const reachable = isChromaReachable()
361
- const installed = isChromaCliInstalled()
362
- const version = installed ? getChromaVersion() : 'N/A'
363
- const dataPath = getChromaDataPath()
364
- const dataExists = existsSync(dataPath)
365
-
366
- const items: Record<string, string> = {
367
- 'Installed': installed ? `Yes (${version})` : 'No',
368
- 'Server': reachable ? `Running (port ${DEFAULT_PORT})` : 'Not running',
369
- 'Managed PID': pid ? String(pid) : 'None',
370
- 'Data Path': dataPath,
371
- 'Data Exists': dataExists ? 'Yes' : 'No',
372
- }
373
-
374
- console.log(infoPanel('ChromaDB Status', items))
375
-
376
- if (!installed) {
377
- console.log()
378
- console.log(warningText('ChromaDB is not installed.'))
379
- console.log(dimText('Install with: ') + theme.primary('claude-brain chroma install'))
380
- } else if (!reachable) {
381
- console.log()
382
- console.log(dimText('Start the server with: ') + theme.primary('claude-brain chroma start'))
383
- }
384
- console.log()
385
- }
386
-
387
- async function chromaInstall(): Promise<void> {
388
- console.log()
389
- console.log(heading('Installing ChromaDB'))
390
- console.log()
391
-
392
- if (isChromaCliInstalled()) {
393
- const version = getChromaVersion()
394
- console.log(successText(`ChromaDB is already installed (${version}).`))
395
- console.log()
396
- return
397
- }
398
-
399
- const python = isPythonInstalled()
400
- if (!python.installed) {
401
- console.log(errorText('Python 3 is required to install ChromaDB.'))
402
- console.log()
403
- console.log(dimText('Install Python 3 first:'))
404
- console.log(` ${theme.primary('macOS:')} brew install python3`)
405
- console.log(` ${theme.primary('Ubuntu:')} sudo apt install python3 python3-pip`)
406
- console.log(` ${theme.primary('Windows:')} https://python.org/downloads`)
407
- console.log()
408
- process.exit(1)
409
- }
410
-
411
- console.log(dimText(`Found ${python.cmd}. Installing chromadb via pip...`))
412
- console.log()
413
-
414
- const pipCommands = python.cmd === 'python3'
415
- ? ['pip3 install chromadb', 'python3 -m pip install chromadb']
416
- : ['pip install chromadb', 'python -m pip install chromadb']
417
-
418
- let installed = false
419
- for (const cmd of pipCommands) {
420
- try {
421
- execSync(cmd, { stdio: 'inherit', timeout: 300_000 })
422
- installed = true
423
- break
424
- } catch {
425
- // Try next command
426
- }
427
- }
428
-
429
- console.log()
430
- if (installed) {
431
- console.log(successText('ChromaDB installed successfully!'))
432
- console.log()
433
- console.log(dimText('Start the server with: ') + theme.primary('claude-brain chroma start'))
434
- } else {
435
- console.log(errorText('Failed to install ChromaDB.'))
436
- console.log(dimText('Try installing manually:'))
437
- console.log(` ${theme.primary('pip install chromadb')}`)
438
- process.exit(1)
439
- }
440
- console.log()
441
- }
442
-
443
- // ── Help ──────────────────────────────────────────────────
444
-
445
- function printChromaHelp(): void {
446
- console.log()
447
- console.log(heading('ChromaDB Management'))
448
- console.log()
449
- console.log(dimText('ChromaDB provides vector storage for semantic search,'))
450
- console.log(dimText('knowledge graph, and advanced intelligence features.'))
451
- console.log()
452
-
453
- const commands = [
454
- ['start', 'Start ChromaDB server in background'],
455
- ['stop', 'Stop the running ChromaDB server'],
456
- ['status', 'Show ChromaDB server status'],
457
- ['install', 'Install ChromaDB (requires Python 3)'],
458
- ]
459
-
460
- const cmdLines = commands
461
- .map(([cmd, desc]) => ` ${theme.primary(cmd!.padEnd(12))} ${dimText(desc!)}`)
462
- .join('\n')
463
-
464
- console.log(theme.bold('Commands:'))
465
- console.log(cmdLines)
466
- console.log()
467
-
468
- console.log(theme.bold('Usage:'))
469
- console.log(` ${dimText('claude-brain chroma install')} ${dimText('Install ChromaDB')}`)
470
- console.log(` ${dimText('claude-brain chroma start')} ${dimText('Start the server')}`)
471
- console.log(` ${dimText('claude-brain chroma status')} ${dimText('Check if running')}`)
472
- console.log(` ${dimText('claude-brain chroma stop')} ${dimText('Stop the server')}`)
473
- console.log()
474
- }
475
-
476
- // ── Auto-start for serve command ──────────────────────────
477
-
478
- /**
479
- * Ensures ChromaDB is running before the MCP server starts.
480
- * Returns true if ChromaDB is reachable after this call, false otherwise.
481
- * Designed to be called from serve.ts does not call process.exit().
482
- */
483
- export async function ensureChromaRunning(options?: { silent?: boolean }): Promise<boolean> {
484
- const log = options?.silent ? () => {} : console.error.bind(console)
485
-
486
- // Already running? Great.
487
- if (isChromaReachable()) {
488
- return true
489
- }
490
-
491
- // Already managed by us but not responding? Clean up stale PID.
492
- const stalePid = getRunningPid()
493
- if (stalePid) {
494
- try {
495
- process.kill(stalePid, 'SIGTERM')
496
- await sleep(1000)
497
- } catch {}
498
- try { unlinkSync(getPidFilePath()) } catch {}
499
- }
500
-
501
- const chromaBinary = getChromaBinary()
502
- if (!chromaBinary) {
503
- log('[ChromaDB] Not installed — running with SQLite fallback.')
504
- log('[ChromaDB] Install with: claude-brain chroma install')
505
- return false
506
- }
507
-
508
- const dataPath = getChromaDataPath()
509
- log(`[ChromaDB] Starting server (port ${DEFAULT_PORT})...`)
510
-
511
- const child = spawn(chromaBinary, ['run', '--path', dataPath, '--port', DEFAULT_PORT], {
512
- detached: true,
513
- stdio: ['ignore', 'pipe', 'pipe'],
514
- env: { ...process.env }
515
- })
516
-
517
- const pid = child.pid
518
- if (!pid) {
519
- log('[ChromaDB] Failed to spawn server process.')
520
- return false
521
- }
522
-
523
- child.unref()
524
- writeFileSync(getPidFilePath(), String(pid), 'utf-8')
525
-
526
- // Wait for server to become reachable (up to 15 seconds)
527
- for (let i = 0; i < 30; i++) {
528
- await sleep(500)
529
- if (isChromaReachable()) {
530
- log(`[ChromaDB] Server started (PID: ${pid})`)
531
- return true
532
- }
533
- // Check if process died
534
- try {
535
- process.kill(pid, 0)
536
- } catch {
537
- break
538
- }
539
- }
540
-
541
- log('[ChromaDB] Server started but not yet responding — will retry connection during initialization.')
542
- return isChromaReachable()
543
- }
544
-
545
- // ── Entry Point ───────────────────────────────────────────
546
-
547
- export async function runChroma(): Promise<void> {
548
- const args = parseArgs(process.argv.slice(3), {
549
- subcommand: { type: 'positional', required: false, description: 'start|stop|status|install|help' },
550
- })
551
- const subcommand: string = args.subcommand ?? 'help'
552
-
553
- switch (subcommand) {
554
- case 'start':
555
- await chromaStart()
556
- break
557
- case 'stop':
558
- await chromaStop()
559
- break
560
- case 'status':
561
- await chromaStatus()
562
- break
563
- case 'install':
564
- await chromaInstall()
565
- break
566
- case 'help':
567
- case '--help':
568
- case '-h':
569
- printChromaHelp()
570
- break
571
- default:
572
- console.log()
573
- console.log(errorText(`Unknown subcommand: ${subcommand}`))
574
- printChromaHelp()
575
- process.exit(1)
576
- }
577
- }
1
+ /**
2
+ * ChromaDB Management Command
3
+ * Start, stop, and manage the ChromaDB server for Claude Brain
4
+ */
5
+
6
+ import { spawn, spawnSync } from 'node:child_process'
7
+ import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'node:fs'
8
+ import { join } from 'node:path'
9
+ import { parseArgs } from 'citty'
10
+ import { getHomePaths } from '@/config/home'
11
+ import {
12
+ theme, heading, successText, errorText, warningText, dimText,
13
+ infoPanel,
14
+ } from '@/cli/ui/index.js'
15
+
16
+ const PID_FILENAME = 'chroma.pid'
17
+ const DEFAULT_PORT = '8000'
18
+
19
+ function getPidFilePath(): string {
20
+ const paths = getHomePaths()
21
+ return join(paths.data, PID_FILENAME)
22
+ }
23
+
24
+ function getChromaDataPath(): string {
25
+ const paths = getHomePaths()
26
+ return paths.chroma
27
+ }
28
+
29
+ /**
30
+ * Find the chroma binary - checks PATH first, then common pip install locations
31
+ */
32
+ function findChromaBinary(): string | null {
33
+ const isWindows = process.platform === 'win32'
34
+ const chromaName = isWindows ? 'chroma.exe' : 'chroma'
35
+
36
+ // Try bare 'chroma' first (on PATH)
37
+ {
38
+ const result = spawnSync('chroma', ['--version'], { stdio: 'pipe', timeout: 5000 })
39
+ if (!result.error && result.status === 0) return 'chroma'
40
+ }
41
+
42
+ const { homedir } = require('os')
43
+ const home = homedir()
44
+
45
+ // Platform-specific search paths
46
+ const candidates: string[] = isWindows
47
+ ? [
48
+ // Windows pip install locations
49
+ join(home, 'AppData', 'Local', 'Programs', 'Python', 'Python39', 'Scripts', chromaName),
50
+ join(home, 'AppData', 'Local', 'Programs', 'Python', 'Python310', 'Scripts', chromaName),
51
+ join(home, 'AppData', 'Local', 'Programs', 'Python', 'Python311', 'Scripts', chromaName),
52
+ join(home, 'AppData', 'Local', 'Programs', 'Python', 'Python312', 'Scripts', chromaName),
53
+ join(home, 'AppData', 'Local', 'Programs', 'Python', 'Python313', 'Scripts', chromaName),
54
+ // Windows user pip install (pip install --user)
55
+ join(home, 'AppData', 'Roaming', 'Python', 'Python39', 'Scripts', chromaName),
56
+ join(home, 'AppData', 'Roaming', 'Python', 'Python310', 'Scripts', chromaName),
57
+ join(home, 'AppData', 'Roaming', 'Python', 'Python311', 'Scripts', chromaName),
58
+ join(home, 'AppData', 'Roaming', 'Python', 'Python312', 'Scripts', chromaName),
59
+ join(home, 'AppData', 'Roaming', 'Python', 'Python313', 'Scripts', chromaName),
60
+ // Scoop / Chocolatey / winget
61
+ join(home, 'scoop', 'shims', chromaName),
62
+ 'C:\\Python39\\Scripts\\' + chromaName,
63
+ 'C:\\Python310\\Scripts\\' + chromaName,
64
+ 'C:\\Python311\\Scripts\\' + chromaName,
65
+ 'C:\\Python312\\Scripts\\' + chromaName,
66
+ 'C:\\Python313\\Scripts\\' + chromaName,
67
+ ]
68
+ : [
69
+ // macOS pip install locations
70
+ join(home, 'Library', 'Python', '3.9', 'bin', chromaName),
71
+ join(home, 'Library', 'Python', '3.10', 'bin', chromaName),
72
+ join(home, 'Library', 'Python', '3.11', 'bin', chromaName),
73
+ join(home, 'Library', 'Python', '3.12', 'bin', chromaName),
74
+ join(home, 'Library', 'Python', '3.13', 'bin', chromaName),
75
+ // Linux pip install locations
76
+ join(home, '.local', 'bin', chromaName),
77
+ '/usr/local/bin/' + chromaName,
78
+ '/opt/homebrew/bin/' + chromaName,
79
+ ]
80
+
81
+ for (const candidate of candidates) {
82
+ try {
83
+ if (existsSync(candidate)) {
84
+ const result = spawnSync(candidate, ['--version'], { stdio: 'pipe', timeout: 5000 })
85
+ if (!result.error && result.status === 0) return candidate
86
+ }
87
+ } catch {}
88
+ }
89
+
90
+ // Try finding via python -m site (works on all platforms)
91
+ const pythonCmd = isWindows ? 'python' : 'python3'
92
+ try {
93
+ const siteResult = spawnSync(pythonCmd, ['-c', 'import site; print(site.getusersitepackages())'], {
94
+ encoding: 'utf-8', stdio: 'pipe', timeout: 5000
95
+ })
96
+ const sitePackages = siteResult.stdout?.trim() || ''
97
+
98
+ if (sitePackages && !siteResult.error && siteResult.status === 0) {
99
+ let binDir: string
100
+ if (isWindows) {
101
+ // Windows: C:\Users\x\AppData\Roaming\Python\Python311\site-packages → Scripts
102
+ binDir = sitePackages.replace(/[\\\/]site-packages$/, '\\Scripts')
103
+ } else {
104
+ // Unix: /Users/x/Library/Python/3.9/lib/python/site-packages → bin
105
+ binDir = sitePackages.replace(/\/lib\/.*/, '/bin')
106
+ }
107
+
108
+ const chromaPath = join(binDir, chromaName)
109
+ if (existsSync(chromaPath)) {
110
+ const verResult = spawnSync(chromaPath, ['--version'], { stdio: 'pipe', timeout: 5000 })
111
+ if (!verResult.error && verResult.status === 0) return chromaPath
112
+ }
113
+ }
114
+ } catch {}
115
+
116
+ return null
117
+ }
118
+
119
+ let _cachedChromaBinary: string | null | undefined = undefined
120
+
121
+ function getChromaBinary(): string | null {
122
+ if (_cachedChromaBinary === undefined) {
123
+ _cachedChromaBinary = findChromaBinary()
124
+ }
125
+ return _cachedChromaBinary
126
+ }
127
+
128
+ function isChromaCliInstalled(): boolean {
129
+ return getChromaBinary() !== null
130
+ }
131
+
132
+ function getChromaVersion(): string {
133
+ const binary = getChromaBinary()
134
+ if (!binary) return 'unknown'
135
+ try {
136
+ const result = spawnSync(binary, ['--version'], { encoding: 'utf-8', stdio: 'pipe', timeout: 5000 })
137
+ if (result.error || result.status !== 0) return 'unknown'
138
+ return result.stdout?.trim() || 'unknown'
139
+ } catch {
140
+ return 'unknown'
141
+ }
142
+ }
143
+
144
+ function isPythonInstalled(): { installed: boolean; cmd: string } {
145
+ for (const cmd of ['python3', 'python']) {
146
+ const result = spawnSync(cmd, ['--version'], { stdio: 'pipe', timeout: 5000 })
147
+ if (!result.error && result.status === 0) {
148
+ return { installed: true, cmd }
149
+ }
150
+ }
151
+ return { installed: false, cmd: 'python3' }
152
+ }
153
+
154
+ function getRunningPid(): number | null {
155
+ const pidPath = getPidFilePath()
156
+ if (!existsSync(pidPath)) return null
157
+
158
+ try {
159
+ const pid = parseInt(readFileSync(pidPath, 'utf-8').trim(), 10)
160
+ if (isNaN(pid)) {
161
+ unlinkSync(pidPath)
162
+ return null
163
+ }
164
+ // Signal 0 tests if process exists without killing it
165
+ process.kill(pid, 0)
166
+ return pid
167
+ } catch {
168
+ // Process not running, clean up stale PID file
169
+ try { unlinkSync(pidPath) } catch {}
170
+ return null
171
+ }
172
+ }
173
+
174
+ function isChromaReachable(): boolean {
175
+ // Try v2 API first (ChromaDB 1.x server), then v1 (older servers)
176
+ for (const apiVersion of ['v2', 'v1']) {
177
+ const result = spawnSync('curl', ['-sf', `http://localhost:${DEFAULT_PORT}/api/${apiVersion}/heartbeat`], {
178
+ stdio: 'pipe',
179
+ timeout: 3000
180
+ })
181
+ if (!result.error && result.status === 0) return true
182
+ }
183
+ return false
184
+ }
185
+
186
+ async function sleep(ms: number): Promise<void> {
187
+ return new Promise(resolve => setTimeout(resolve, ms))
188
+ }
189
+
190
+ // ── Subcommands ───────────────────────────────────────────
191
+
192
+ async function chromaStart(): Promise<void> {
193
+ console.log()
194
+ console.log(heading('Starting ChromaDB Server'))
195
+ console.log()
196
+
197
+ // Check if already running
198
+ const existingPid = getRunningPid()
199
+ if (existingPid) {
200
+ console.log(warningText(`ChromaDB is already running (PID: ${existingPid})`))
201
+ console.log()
202
+ return
203
+ }
204
+
205
+ if (isChromaReachable()) {
206
+ console.log(warningText('A ChromaDB server is already running on port ' + DEFAULT_PORT))
207
+ console.log()
208
+ return
209
+ }
210
+
211
+ // Check if chroma CLI is installed
212
+ const chromaBinary = getChromaBinary()
213
+ if (!chromaBinary) {
214
+ console.log(errorText('ChromaDB CLI is not installed or not found on PATH.'))
215
+ console.log()
216
+ console.log(dimText('Install it with:'))
217
+ console.log(` ${theme.primary('pip install chromadb')}`)
218
+ console.log()
219
+ console.log(dimText('Or run:'))
220
+ console.log(` ${theme.primary('claude-brain chroma install')}`)
221
+ console.log()
222
+ process.exit(1)
223
+ }
224
+
225
+ const dataPath = getChromaDataPath()
226
+
227
+ console.log(dimText(`Binary: ${chromaBinary}`))
228
+ console.log(dimText(`Data path: ${dataPath}`))
229
+ console.log(dimText(`Port: ${DEFAULT_PORT}`))
230
+ console.log()
231
+
232
+ // Start ChromaDB server in background
233
+ const child = spawn(chromaBinary, ['run', '--path', dataPath, '--port', DEFAULT_PORT], {
234
+ detached: true,
235
+ stdio: ['ignore', 'pipe', 'pipe'],
236
+ env: { ...process.env }
237
+ })
238
+
239
+ const pid = child.pid
240
+ if (!pid) {
241
+ console.log(errorText('Failed to start ChromaDB server — no PID returned.'))
242
+ console.log()
243
+ process.exit(1)
244
+ }
245
+
246
+ // Detach the child process so it runs independently
247
+ child.unref()
248
+
249
+ // Save PID file
250
+ writeFileSync(getPidFilePath(), String(pid), 'utf-8')
251
+
252
+ // Capture early output for error detection
253
+ let startupOutput = ''
254
+ child.stderr?.on('data', (chunk: Buffer) => {
255
+ startupOutput += chunk.toString()
256
+ })
257
+ child.stdout?.on('data', (chunk: Buffer) => {
258
+ startupOutput += chunk.toString()
259
+ })
260
+
261
+ // Wait up to 15 seconds for server to become reachable
262
+ let ready = false
263
+ for (let i = 0; i < 30; i++) {
264
+ await sleep(500)
265
+ if (isChromaReachable()) {
266
+ ready = true
267
+ break
268
+ }
269
+ // Check if process exited early
270
+ try {
271
+ process.kill(pid, 0)
272
+ } catch {
273
+ break
274
+ }
275
+ }
276
+
277
+ if (ready) {
278
+ console.log(successText(`ChromaDB server started (PID: ${pid})`))
279
+ console.log()
280
+ console.log(dimText('The server is running in the background.'))
281
+ console.log(dimText('Stop it with: ') + theme.primary('claude-brain chroma stop'))
282
+ } else {
283
+ // Check if process is still alive
284
+ let alive = false
285
+ try {
286
+ process.kill(pid, 0)
287
+ alive = true
288
+ } catch {}
289
+
290
+ if (alive) {
291
+ console.log(warningText('ChromaDB server started but not yet responding.'))
292
+ console.log(dimText(`PID: ${pid} it may still be initializing.`))
293
+ console.log(dimText('Check with: ') + theme.primary('claude-brain chroma status'))
294
+ } else {
295
+ console.log(errorText('ChromaDB server failed to start.'))
296
+ if (startupOutput) {
297
+ console.log(dimText('Output:'))
298
+ console.log(dimText(startupOutput.slice(0, 500)))
299
+ }
300
+ try { unlinkSync(getPidFilePath()) } catch {}
301
+ process.exit(1)
302
+ }
303
+ }
304
+ console.log()
305
+ }
306
+
307
+ async function chromaStop(): Promise<void> {
308
+ console.log()
309
+ console.log(heading('Stopping ChromaDB Server'))
310
+ console.log()
311
+
312
+ const pid = getRunningPid()
313
+ if (!pid) {
314
+ if (isChromaReachable()) {
315
+ console.log(warningText('A ChromaDB server is running on port ' + DEFAULT_PORT + ' but was not started by claude-brain.'))
316
+ console.log(dimText('Kill it manually if needed.'))
317
+ } else {
318
+ console.log(dimText('No ChromaDB server is currently running.'))
319
+ }
320
+ console.log()
321
+ return
322
+ }
323
+
324
+ try {
325
+ process.kill(pid, 'SIGTERM')
326
+
327
+ // Wait up to 5 seconds for graceful shutdown
328
+ for (let i = 0; i < 10; i++) {
329
+ await sleep(500)
330
+ try {
331
+ process.kill(pid, 0)
332
+ } catch {
333
+ // Process exited
334
+ break
335
+ }
336
+ }
337
+
338
+ // Force kill if still running
339
+ try {
340
+ process.kill(pid, 0)
341
+ // Still alive, force kill
342
+ process.kill(pid, 'SIGKILL')
343
+ } catch {
344
+ // Already dead, good
345
+ }
346
+
347
+ try { unlinkSync(getPidFilePath()) } catch {}
348
+
349
+ console.log(successText(`ChromaDB server stopped (PID: ${pid})`))
350
+ } catch (error) {
351
+ console.log(errorText(`Failed to stop ChromaDB: ${error instanceof Error ? error.message : String(error)}`))
352
+ try { unlinkSync(getPidFilePath()) } catch {}
353
+ }
354
+ console.log()
355
+ }
356
+
357
+ async function chromaStatus(): Promise<void> {
358
+ console.log()
359
+
360
+ const pid = getRunningPid()
361
+ const reachable = isChromaReachable()
362
+ const installed = isChromaCliInstalled()
363
+ const version = installed ? getChromaVersion() : 'N/A'
364
+ const dataPath = getChromaDataPath()
365
+ const dataExists = existsSync(dataPath)
366
+
367
+ const items: Record<string, string> = {
368
+ 'Installed': installed ? `Yes (${version})` : 'No',
369
+ 'Server': reachable ? `Running (port ${DEFAULT_PORT})` : 'Not running',
370
+ 'Managed PID': pid ? String(pid) : 'None',
371
+ 'Data Path': dataPath,
372
+ 'Data Exists': dataExists ? 'Yes' : 'No',
373
+ }
374
+
375
+ console.log(infoPanel('ChromaDB Status', items))
376
+
377
+ if (!installed) {
378
+ console.log()
379
+ console.log(warningText('ChromaDB is not installed.'))
380
+ console.log(dimText('Install with: ') + theme.primary('claude-brain chroma install'))
381
+ } else if (!reachable) {
382
+ console.log()
383
+ console.log(dimText('Start the server with: ') + theme.primary('claude-brain chroma start'))
384
+ }
385
+ console.log()
386
+ }
387
+
388
+ async function chromaInstall(): Promise<void> {
389
+ console.log()
390
+ console.log(heading('Installing ChromaDB'))
391
+ console.log()
392
+
393
+ if (isChromaCliInstalled()) {
394
+ const version = getChromaVersion()
395
+ console.log(successText(`ChromaDB is already installed (${version}).`))
396
+ console.log()
397
+ return
398
+ }
399
+
400
+ const python = isPythonInstalled()
401
+ if (!python.installed) {
402
+ console.log(errorText('Python 3 is required to install ChromaDB.'))
403
+ console.log()
404
+ console.log(dimText('Install Python 3 first:'))
405
+ console.log(` ${theme.primary('macOS:')} brew install python3`)
406
+ console.log(` ${theme.primary('Ubuntu:')} sudo apt install python3 python3-pip`)
407
+ console.log(` ${theme.primary('Windows:')} https://python.org/downloads`)
408
+ console.log()
409
+ process.exit(1)
410
+ }
411
+
412
+ console.log(dimText(`Found ${python.cmd}. Installing chromadb via pip...`))
413
+ console.log()
414
+
415
+ const pipCommands = python.cmd === 'python3'
416
+ ? ['pip3 install chromadb', 'python3 -m pip install chromadb']
417
+ : ['pip install chromadb', 'python -m pip install chromadb']
418
+
419
+ let installed = false
420
+ for (const cmd of pipCommands) {
421
+ const parts = cmd.split(' ')
422
+ const result = spawnSync(parts[0]!, parts.slice(1), { stdio: 'inherit', timeout: 300_000 })
423
+ if (!result.error && result.status === 0) {
424
+ installed = true
425
+ break
426
+ }
427
+ // Try next command
428
+ }
429
+
430
+ console.log()
431
+ if (installed) {
432
+ console.log(successText('ChromaDB installed successfully!'))
433
+ console.log()
434
+ console.log(dimText('Start the server with: ') + theme.primary('claude-brain chroma start'))
435
+ } else {
436
+ console.log(errorText('Failed to install ChromaDB.'))
437
+ console.log(dimText('Try installing manually:'))
438
+ console.log(` ${theme.primary('pip install chromadb')}`)
439
+ process.exit(1)
440
+ }
441
+ console.log()
442
+ }
443
+
444
+ // ── Help ──────────────────────────────────────────────────
445
+
446
+ function printChromaHelp(): void {
447
+ console.log()
448
+ console.log(heading('ChromaDB Management'))
449
+ console.log()
450
+ console.log(dimText('ChromaDB provides vector storage for semantic search,'))
451
+ console.log(dimText('knowledge graph, and advanced intelligence features.'))
452
+ console.log()
453
+
454
+ const commands = [
455
+ ['start', 'Start ChromaDB server in background'],
456
+ ['stop', 'Stop the running ChromaDB server'],
457
+ ['status', 'Show ChromaDB server status'],
458
+ ['install', 'Install ChromaDB (requires Python 3)'],
459
+ ]
460
+
461
+ const cmdLines = commands
462
+ .map(([cmd, desc]) => ` ${theme.primary(cmd!.padEnd(12))} ${dimText(desc!)}`)
463
+ .join('\n')
464
+
465
+ console.log(theme.bold('Commands:'))
466
+ console.log(cmdLines)
467
+ console.log()
468
+
469
+ console.log(theme.bold('Usage:'))
470
+ console.log(` ${dimText('claude-brain chroma install')} ${dimText('Install ChromaDB')}`)
471
+ console.log(` ${dimText('claude-brain chroma start')} ${dimText('Start the server')}`)
472
+ console.log(` ${dimText('claude-brain chroma status')} ${dimText('Check if running')}`)
473
+ console.log(` ${dimText('claude-brain chroma stop')} ${dimText('Stop the server')}`)
474
+ console.log()
475
+ }
476
+
477
+ // ── Auto-start for serve command ──────────────────────────
478
+
479
+ /**
480
+ * Ensures ChromaDB is running before the MCP server starts.
481
+ * Returns true if ChromaDB is reachable after this call, false otherwise.
482
+ * Designed to be called from serve.ts — does not call process.exit().
483
+ */
484
+ export async function ensureChromaRunning(options?: { silent?: boolean }): Promise<boolean> {
485
+ const log = options?.silent ? () => {} : console.error.bind(console)
486
+
487
+ // Already running? Great.
488
+ if (isChromaReachable()) {
489
+ return true
490
+ }
491
+
492
+ // Already managed by us but not responding? Clean up stale PID.
493
+ const stalePid = getRunningPid()
494
+ if (stalePid) {
495
+ try {
496
+ process.kill(stalePid, 'SIGTERM')
497
+ await sleep(1000)
498
+ } catch {}
499
+ try { unlinkSync(getPidFilePath()) } catch {}
500
+ }
501
+
502
+ const chromaBinary = getChromaBinary()
503
+ if (!chromaBinary) {
504
+ log('[ChromaDB] Not installed — running with SQLite fallback.')
505
+ log('[ChromaDB] Install with: claude-brain chroma install')
506
+ return false
507
+ }
508
+
509
+ const dataPath = getChromaDataPath()
510
+ log(`[ChromaDB] Starting server (port ${DEFAULT_PORT})...`)
511
+
512
+ const child = spawn(chromaBinary, ['run', '--path', dataPath, '--port', DEFAULT_PORT], {
513
+ detached: true,
514
+ stdio: ['ignore', 'pipe', 'pipe'],
515
+ env: { ...process.env }
516
+ })
517
+
518
+ const pid = child.pid
519
+ if (!pid) {
520
+ log('[ChromaDB] Failed to spawn server process.')
521
+ return false
522
+ }
523
+
524
+ child.unref()
525
+ writeFileSync(getPidFilePath(), String(pid), 'utf-8')
526
+
527
+ // Wait for server to become reachable (up to 15 seconds)
528
+ for (let i = 0; i < 30; i++) {
529
+ await sleep(500)
530
+ if (isChromaReachable()) {
531
+ log(`[ChromaDB] Server started (PID: ${pid})`)
532
+ return true
533
+ }
534
+ // Check if process died
535
+ try {
536
+ process.kill(pid, 0)
537
+ } catch {
538
+ break
539
+ }
540
+ }
541
+
542
+ log('[ChromaDB] Server started but not yet responding — will retry connection during initialization.')
543
+ return isChromaReachable()
544
+ }
545
+
546
+ // ── Entry Point ───────────────────────────────────────────
547
+
548
+ export async function runChroma(): Promise<void> {
549
+ const args = parseArgs(process.argv.slice(3), {
550
+ subcommand: { type: 'positional', required: false, description: 'start|stop|status|install|help' },
551
+ })
552
+ const subcommand: string = args.subcommand ?? 'help'
553
+
554
+ switch (subcommand) {
555
+ case 'start':
556
+ await chromaStart()
557
+ break
558
+ case 'stop':
559
+ await chromaStop()
560
+ break
561
+ case 'status':
562
+ await chromaStatus()
563
+ break
564
+ case 'install':
565
+ await chromaInstall()
566
+ break
567
+ case 'help':
568
+ case '--help':
569
+ case '-h':
570
+ printChromaHelp()
571
+ break
572
+ default:
573
+ console.log()
574
+ console.log(errorText(`Unknown subcommand: ${subcommand}`))
575
+ printChromaHelp()
576
+ process.exit(1)
577
+ }
578
+ }