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,463 +1,466 @@
1
- import { loadConfig } from '@/config'
2
- import { createLogger, createComponentLogger } from '@/utils'
3
- import { GlobalErrorHandler, CleanupManager } from '@/utils'
4
- import { ClaudeBrainMCPServer } from '@/server'
5
- import { initializeServices, shutdownServices, getVaultService, getMemoryService } from '@/server/services'
6
- import { createOrchestrator, type Orchestrator } from '@/orchestrator'
7
- import { ensureHomeDirectory } from '@/cli/auto-setup'
8
- import { ServerPidManager } from '@/server/pid-manager'
9
-
10
- const BANNER = `
11
- ╔═══════════════════════════════════════════════════════╗
12
- ║ CLAUDE BRAIN ║
13
- ║ Local Development Assistant with Memory ║
14
- ╚═══════════════════════════════════════════════════════╝
15
- `
16
-
17
- export async function runServe() {
18
- // Auto-initialize home directory on first run
19
- ensureHomeDirectory()
20
-
21
- const httpOnly = process.argv.includes('--http-only')
22
- const pidManager = new ServerPidManager()
23
- const existingInstance = pidManager.getRunningInstance()
24
-
25
- // ── Decision tree ──────────────────────────────────────────
26
- // httpOnly + existing daemon → exit (duplicate daemon)
27
- // httpOnly + no daemon → start as daemon (full services + HTTP + idle watchdog)
28
- // !httpOnly + healthy daemon → run as proxy (thin MCP bridge, no services)
29
- // !httpOnly + no daemon → start as daemon + MCP stdio (become daemon)
30
-
31
- if (httpOnly && existingInstance) {
32
- console.error(`[claude-brain] Primary instance already running (PID ${existingInstance.pid}). Exiting.`)
33
- process.exit(0)
34
- }
35
-
36
- if (!httpOnly && existingInstance) {
37
- // Check if existing daemon is healthy before committing to proxy mode
38
- const healthy = await isDaemonHealthy(existingInstance.port)
39
- if (healthy) {
40
- return runAsProxy(existingInstance.port)
41
- }
42
- // Daemon not healthy — clean up stale PID and start as daemon
43
- pidManager.cleanup()
44
- }
45
-
46
- // ── Start as daemon ────────────────────────────────────────
47
- return runAsDaemon(httpOnly, pidManager)
48
- }
49
-
50
- /** Check if the daemon at the given port is responsive, initialized, and can serve MCP tools */
51
- async function isDaemonHealthy(port: number): Promise<boolean> {
52
- try {
53
- const healthRes = await fetch(`http://localhost:${port}/api/health`, {
54
- signal: AbortSignal.timeout(2000),
55
- })
56
- const healthJson = await healthRes.json() as any
57
- if (healthJson.success !== true || healthJson.initialized !== true) return false
58
-
59
- // Verify MCP proxy endpoints actually work (old versions return 404)
60
- const toolsRes = await fetch(`http://localhost:${port}/api/mcp/list-tools`, {
61
- signal: AbortSignal.timeout(2000),
62
- })
63
- const toolsJson = await toolsRes.json() as any
64
- return toolsJson.success === true && Array.isArray(toolsJson.data?.tools)
65
- } catch {
66
- return false
67
- }
68
- }
69
-
70
- /** Run as thin MCP proxy — forward all MCP calls to daemon via HTTP */
71
- async function runAsProxy(daemonPort: number) {
72
- const config = await loadConfig()
73
- const logger = createLogger(config.logLevel, config.logFilePath)
74
- const mainLogger = createComponentLogger(logger, 'proxy')
75
-
76
- mainLogger.info({ daemonPort }, 'Starting as MCP proxy → daemon')
77
-
78
- const { McpProxyServer } = await import('@/server/mcp-proxy')
79
-
80
- let isStopping = false
81
- const proxy = new McpProxyServer({
82
- daemonUrl: `http://localhost:${daemonPort}`,
83
- serverName: config.serverName,
84
- serverVersion: config.serverVersion,
85
- onStdinClose: () => {
86
- if (isStopping) return
87
- isStopping = true
88
- mainLogger.info('stdin closed — proxy exiting')
89
- proxy.stop().finally(() => process.exit(0))
90
- },
91
- })
92
-
93
- const stopProxy = async (signal: string) => {
94
- if (isStopping) return
95
- isStopping = true
96
- mainLogger.info({ signal }, 'Proxy shutting down')
97
- await proxy.stop()
98
- process.exit(0)
99
- }
100
-
101
- process.on('SIGTERM', () => stopProxy('SIGTERM'))
102
- process.on('SIGINT', () => stopProxy('SIGINT'))
103
- if (process.platform !== 'win32') {
104
- process.on('SIGHUP', () => stopProxy('SIGHUP'))
105
- }
106
-
107
- await proxy.start()
108
- mainLogger.info('MCP proxy ready — forwarding to daemon')
109
- }
110
-
111
- /** Run as full daemon with services, HTTP, and optionally MCP stdio */
112
- async function runAsDaemon(httpOnly: boolean, pidManager: ServerPidManager) {
113
- // Auto-install Claude Code hooks (idempotent, non-fatal)
114
- try {
115
- const { installHooks } = await import('@/hooks/installer')
116
- const hookResult = installHooks()
117
- if (hookResult.message !== 'Hooks already installed') {
118
- console.error(`[claude-brain] ${hookResult.message}`)
119
- }
120
- } catch (error) {
121
- console.error(`[claude-brain] Hook auto-install skipped: ${error instanceof Error ? error.message : 'unknown error'}`)
122
- }
123
-
124
- // Auto-install shell auto-start if not already present
125
- try {
126
- const { isAutoStartInstalled, installAutoStart } = await import('@/cli/auto-start')
127
- if (!isAutoStartInstalled()) {
128
- const result = installAutoStart()
129
- if (result.success && result.profilePath) {
130
- console.error(`[claude-brain] Auto-start installed in ${result.profilePath}`)
131
- }
132
- }
133
- } catch (error) {
134
- // Non-fatal — auto-start is a convenience feature
135
- }
136
-
137
- // Auto-install CLAUDE.md if not present
138
- try {
139
- const { existsSync, readFileSync, writeFileSync, mkdirSync } = await import('node:fs')
140
- const { join, resolve, dirname } = await import('node:path')
141
- const { homedir } = await import('node:os')
142
- const { fileURLToPath } = await import('node:url')
143
- const claudeMdDest = join(homedir(), '.claude', 'CLAUDE.md')
144
- const pkgRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', '..')
145
- const claudeMdSrc = join(pkgRoot, 'assets', 'CLAUDE.md')
146
- if (existsSync(claudeMdSrc) && !existsSync(claudeMdDest)) {
147
- mkdirSync(join(homedir(), '.claude'), { recursive: true })
148
- writeFileSync(claudeMdDest, readFileSync(claudeMdSrc, 'utf-8'), 'utf-8')
149
- console.error('[claude-brain] CLAUDE.md installed to ~/.claude/CLAUDE.md')
150
- }
151
- } catch {
152
- // Non-fatal
153
- }
154
-
155
- const config = await loadConfig()
156
-
157
- if (config.logLevel === 'debug' || config.logLevel === 'info') {
158
- console.error(BANNER)
159
- }
160
-
161
- const logger = createLogger(config.logLevel, config.logFilePath)
162
- const mainLogger = createComponentLogger(logger, 'main')
163
-
164
- new GlobalErrorHandler(logger)
165
- const cleanup = new CleanupManager(logger)
166
-
167
- mainLogger.info({
168
- serverName: config.serverName,
169
- serverVersion: config.serverVersion,
170
- vaultPath: config.vaultPath,
171
- nodeEnv: config.nodeEnv
172
- }, 'Claude Brain starting...')
173
-
174
- mainLogger.info({
175
- logLevel: config.logLevel,
176
- enableFileWatch: config.enableFileWatch,
177
- cacheSize: config.cacheSize
178
- }, 'Configuration loaded')
179
-
180
- // Only start ChromaDB if explicitly enabled in config
181
- let chromaReady = false
182
- if (config.chromadb?.enabled) {
183
- mainLogger.info('ChromaDB enabled, ensuring it is available...')
184
- const { ensureChromaRunning } = await import('@/cli/commands/chroma')
185
- chromaReady = await ensureChromaRunning({ silent: process.env.NODE_ENV === 'production' })
186
- mainLogger.info({ chromaReady }, chromaReady ? 'ChromaDB is ready' : 'ChromaDB not available, using SQLite fallback')
187
- } else {
188
- mainLogger.info('Using SQLite FTS5 storage (ChromaDB disabled)')
189
- }
190
-
191
- mainLogger.info('Initializing services...')
192
- await initializeServices(config, logger)
193
- mainLogger.info('Services initialized successfully')
194
-
195
- // Initialize orchestrator for event-driven features
196
- let orchestrator: Orchestrator | null = null
197
- try {
198
- const vault = getVaultService()
199
- const memory = getMemoryService()
200
-
201
- orchestrator = createOrchestrator(logger, vault, memory, {
202
- maxConcurrentOperations: 5,
203
- debounceMs: 5000
204
- })
205
-
206
- await orchestrator.initialize()
207
- mainLogger.info('Orchestrator initialized successfully')
208
- } catch (error) {
209
- mainLogger.warn({ error }, 'Failed to initialize orchestrator, continuing without event-driven features')
210
- }
211
-
212
- const mcpServer = new ClaudeBrainMCPServer(config, logger, {
213
- events: {
214
- onConnectionStateChange: (state) => {
215
- mainLogger.info({ state }, 'MCP connection state changed')
216
- },
217
- onError: (error) => {
218
- mainLogger.error({ error: error.message }, 'MCP server error')
219
- },
220
- onRequest: (toolName, requestId) => {
221
- pidManager.touchActivity()
222
- mainLogger.debug({ toolName, requestId }, 'Tool request received')
223
- },
224
- onResponse: (toolName, requestId, durationMs) => {
225
- mainLogger.debug({ toolName, requestId, durationMs }, 'Tool response sent')
226
- }
227
- }
228
- })
229
-
230
- cleanup.register(async () => {
231
- await mcpServer.stop()
232
- })
233
-
234
- // Register orchestrator cleanup before services shutdown
235
- if (orchestrator) {
236
- cleanup.register(async () => {
237
- orchestrator!.shutdown()
238
- mainLogger.info('Orchestrator shut down')
239
- })
240
- }
241
-
242
- cleanup.register(async () => {
243
- await shutdownServices()
244
- })
245
-
246
- // ── Idempotent shutdown guard ────────────────────────────
247
- let isShuttingDown = false
248
- const shutdown = async (signal: string) => {
249
- if (isShuttingDown) return
250
- isShuttingDown = true
251
- mainLogger.info({ signal }, 'Shutting down...')
252
- await cleanup.cleanup()
253
- process.exit(0)
254
- }
255
-
256
- // ── Primary daemon: HTTP server + background services ────
257
- // Write PID file and register cleanup
258
- pidManager.writePidFile(config.port)
259
- pidManager.registerCleanupHandlers()
260
-
261
- cleanup.register(async () => {
262
- pidManager.cleanup()
263
- })
264
-
265
- // Start HTTP API server
266
- const { HttpApiServer } = await import('@/server/http-api')
267
- const httpServer = new HttpApiServer(config, logger)
268
-
269
- // Wire activity tracker to PID manager for idle watchdog
270
- httpServer.setActivityTracker(() => pidManager.touchActivity())
271
-
272
- cleanup.register(async () => {
273
- await httpServer.stop()
274
- })
275
-
276
- // Phase 21: Use session tracker from services
277
- {
278
- const { getSessionTracker } = await import('@/server/services')
279
- const sessionTracker = getSessionTracker()
280
- if (sessionTracker) {
281
- httpServer.setSessionTracker(sessionTracker)
282
- mainLogger.info('Session tracker wired to HTTP server')
283
- }
284
- }
285
-
286
- // Phase 28: Wire code intelligence to HTTP server
287
- {
288
- const { getCodeIndexer, getCodeQuery } = await import('@/server/services')
289
- const codeIndexer = getCodeIndexer()
290
- const codeQuery = getCodeQuery()
291
- if (codeIndexer && codeQuery) {
292
- httpServer.setCodeIntelligence(codeIndexer, codeQuery)
293
- mainLogger.info('Code intelligence wired to HTTP server')
294
- }
295
- }
296
-
297
- // Phase 29: Wire code linker to HTTP server
298
- {
299
- const { getCodeLinker } = await import('@/server/services')
300
- const codeLinker = getCodeLinker()
301
- if (codeLinker) {
302
- httpServer.setCodeLinker(codeLinker)
303
- mainLogger.info('Code linker wired to HTTP server')
304
- }
305
- }
306
-
307
- // Phase 30: Activity log pruning on startup + periodic cleanup
308
- {
309
- const memory = getMemoryService()
310
- if (memory?.isInitialized()) {
311
- try {
312
- const { startPeriodicPruning } = await import('@/memory/pruning')
313
- const db = memory.database.getDb()
314
- const stopPruning = startPeriodicPruning(db, logger, 30)
315
- cleanup.register(async () => {
316
- stopPruning()
317
- mainLogger.info('Activity log pruning stopped')
318
- })
319
- mainLogger.info('Activity log pruning initialized')
320
- } catch (error) {
321
- mainLogger.warn({ error }, 'Failed to initialize activity log pruning')
322
- }
323
- }
324
- }
325
-
326
- // Phase 30: Optional LLM compression
327
- if (config.compression?.enabled) {
328
- try {
329
- const { ObservationCompressor } = await import('@/memory/compression')
330
- const { getBrainRouter } = await import('@/routing/router')
331
- const compressor = new ObservationCompressor(config.compression, logger)
332
- const router = getBrainRouter(logger)
333
- router.setCompressor(compressor)
334
- mainLogger.info({ provider: config.compression.provider, model: config.compression.model }, 'LLM compression enabled')
335
- } catch (error) {
336
- mainLogger.warn({ error }, 'Failed to initialize LLM compression')
337
- }
338
- }
339
-
340
- // Phase 31: Auto-update checker
341
- let autoUpdater: InstanceType<typeof import('@/server/auto-updater').AutoUpdater> | null = null
342
- if (config.autoUpdate?.enabled !== false) {
343
- try {
344
- const { AutoUpdater } = await import('@/server/auto-updater')
345
- autoUpdater = new AutoUpdater(
346
- {
347
- enabled: config.autoUpdate?.enabled ?? true,
348
- checkIntervalHours: config.autoUpdate?.checkIntervalHours ?? 24,
349
- autoRestart: config.autoUpdate?.autoRestart ?? true,
350
- },
351
- mainLogger
352
- )
353
-
354
- // Check for updates on startup (non-blocking)
355
- autoUpdater.check().then(result => {
356
- if (result.updateAvailable && result.latestVersion) {
357
- mainLogger.info(
358
- { current: result.currentVersion, latest: result.latestVersion },
359
- 'Update available! Run: claude-brain update'
360
- )
361
- }
362
- }).catch(() => {})
363
-
364
- // Schedule periodic checks
365
- autoUpdater.schedulePeriodicCheck()
366
-
367
- cleanup.register(async () => {
368
- autoUpdater?.stopPeriodicCheck()
369
- mainLogger.info('Auto-updater stopped')
370
- })
371
-
372
- mainLogger.info('Auto-updater initialized')
373
- } catch (error) {
374
- mainLogger.warn({ error }, 'Failed to initialize auto-updater')
375
- }
376
- }
377
-
378
- // Start HTTP server after MCP server is ready
379
- setTimeout(async () => {
380
- try {
381
- await httpServer.start()
382
- mainLogger.info({ port: config.port }, 'HTTP API server started')
383
-
384
- // Drain hook queue after HTTP server is ready
385
- try {
386
- const { drainQueue } = await import('@/hooks/queue')
387
- const drained = await drainQueue(config.port)
388
- if (drained > 0) {
389
- mainLogger.info({ drained }, 'Drained hook queue')
390
- }
391
- } catch (error) {
392
- mainLogger.debug({ error }, 'No hook queue to drain')
393
- }
394
- } catch (error: any) {
395
- // EADDRINUSE: kill the stale process and retry once
396
- if (error?.code === 'EADDRINUSE' || String(error).includes('EADDRINUSE')) {
397
- mainLogger.warn({ port: config.port }, 'Port in use — killing stale process and retrying')
398
- try {
399
- const { killProcessOnPort } = await import('@/utils/kill-port')
400
- const killed = killProcessOnPort(config.port)
401
- if (killed.length > 0) {
402
- mainLogger.info({ killed }, 'Killed stale process(es) on port')
403
- }
404
- await new Promise(r => setTimeout(r, 1000))
405
- await httpServer.start()
406
- mainLogger.info({ port: config.port }, 'HTTP API server started (after recovery)')
407
-
408
- // Drain hook queue on retry success too
409
- try {
410
- const { drainQueue } = await import('@/hooks/queue')
411
- const drained = await drainQueue(config.port)
412
- if (drained > 0) {
413
- mainLogger.info({ drained }, 'Drained hook queue')
414
- }
415
- } catch {}
416
- } catch (retryError) {
417
- mainLogger.error({ error: retryError }, 'Failed to start HTTP API server after recovery — MCP stdio still works')
418
- }
419
- } else {
420
- mainLogger.error({ error }, 'Failed to start HTTP API server')
421
- }
422
- }
423
- }, 2000)
424
-
425
- // ── Signal handlers ──────────────────────────────────────
426
- process.on('SIGTERM', () => shutdown('SIGTERM'))
427
- process.on('SIGINT', () => shutdown('SIGINT'))
428
- if (process.platform !== 'win32') {
429
- process.on('SIGHUP', () => shutdown('SIGHUP'))
430
- }
431
-
432
- if (httpOnly) {
433
- // HTTP-only daemon mode: no MCP stdio. Use idle watchdog instead of infinite keepAlive.
434
- mainLogger.info('HTTP-only mode — MCP stdio disabled (background daemon)')
435
-
436
- const idleTimeoutMs = (config.daemon?.idleTimeoutMinutes ?? 30) * 60 * 1000
437
- if (idleTimeoutMs > 0) {
438
- const watchdog = setInterval(() => {
439
- const idleMs = pidManager.getIdleMs()
440
- if (idleMs >= idleTimeoutMs) {
441
- mainLogger.info({ idleMs, idleTimeoutMs }, 'Idle timeout reached — shutting down daemon')
442
- clearInterval(watchdog)
443
- shutdown('idle-timeout')
444
- }
445
- }, 60_000)
446
- cleanup.register(async () => clearInterval(watchdog))
447
- mainLogger.info({ idleTimeoutMinutes: config.daemon?.idleTimeoutMinutes ?? 30 }, 'Idle watchdog started')
448
- } else {
449
- // idleTimeoutMinutes=0 means never self-terminate (old behavior)
450
- const keepAlive = setInterval(() => {}, 60_000 * 60)
451
- cleanup.register(async () => clearInterval(keepAlive))
452
- mainLogger.info('Idle watchdog disabled (idleTimeoutMinutes=0)')
453
- }
454
- } else {
455
- // MCP stdio mode: start MCP server and detect stdin closure
456
- await mcpServer.start()
457
- mainLogger.info('Claude Brain MCP server ready and listening for connections')
458
-
459
- // Detect stdin close — Claude Code disconnected
460
- process.stdin.on('end', () => shutdown('stdin-end'))
461
- process.stdin.on('close', () => shutdown('stdin-close'))
462
- }
463
- }
1
+ import { loadConfig } from '@/config'
2
+ import { createLogger, createComponentLogger } from '@/utils'
3
+ import { GlobalErrorHandler, CleanupManager } from '@/utils'
4
+ import { ClaudeBrainMCPServer } from '@/server'
5
+ import { initializeServices, shutdownServices, getVaultService, getMemoryService } from '@/server/services'
6
+ import { createOrchestrator, type Orchestrator } from '@/orchestrator'
7
+ import { ensureHomeDirectory } from '@/cli/auto-setup'
8
+ import { ServerPidManager } from '@/server/pid-manager'
9
+
10
+ const BANNER = `
11
+ ╔═══════════════════════════════════════════════════════╗
12
+ ║ CLAUDE BRAIN ║
13
+ ║ Local Development Assistant with Memory ║
14
+ ╚═══════════════════════════════════════════════════════╝
15
+ `
16
+
17
+ export async function runServe() {
18
+ // Auto-initialize home directory on first run
19
+ ensureHomeDirectory()
20
+
21
+ const httpOnly = process.argv.includes('--http-only')
22
+ const pidManager = new ServerPidManager()
23
+ const existingInstance = pidManager.getRunningInstance()
24
+
25
+ // ── Decision tree ──────────────────────────────────────────
26
+ // httpOnly + existing daemon → exit (duplicate daemon)
27
+ // httpOnly + no daemon → start as daemon (full services + HTTP + idle watchdog)
28
+ // !httpOnly + healthy daemon → run as proxy (thin MCP bridge, no services)
29
+ // !httpOnly + no daemon → start as daemon + MCP stdio (become daemon)
30
+
31
+ if (httpOnly && existingInstance) {
32
+ console.error(`[claude-brain] Primary instance already running (PID ${existingInstance.pid}). Exiting.`)
33
+ process.exit(0)
34
+ }
35
+
36
+ if (!httpOnly && existingInstance) {
37
+ // Check if existing daemon is healthy before committing to proxy mode
38
+ const healthy = await isDaemonHealthy(existingInstance.port)
39
+ if (healthy) {
40
+ return runAsProxy(existingInstance.port)
41
+ }
42
+ // Daemon not healthy — clean up stale PID and start as daemon
43
+ pidManager.cleanup()
44
+ }
45
+
46
+ // ── Start as daemon ────────────────────────────────────────
47
+ return runAsDaemon(httpOnly, pidManager)
48
+ }
49
+
50
+ /** Check if the daemon at the given port is responsive, initialized, and can serve MCP tools */
51
+ async function isDaemonHealthy(port: number): Promise<boolean> {
52
+ try {
53
+ const healthRes = await fetch(`http://localhost:${port}/api/health`, {
54
+ signal: AbortSignal.timeout(2000),
55
+ })
56
+ const healthJson = await healthRes.json() as any
57
+ if (healthJson.success !== true || healthJson.initialized !== true) return false
58
+
59
+ // Verify MCP proxy endpoints actually work (old versions return 404)
60
+ const toolsRes = await fetch(`http://localhost:${port}/api/mcp/list-tools`, {
61
+ signal: AbortSignal.timeout(2000),
62
+ })
63
+ const toolsJson = await toolsRes.json() as any
64
+ return toolsJson.success === true && Array.isArray(toolsJson.data?.tools)
65
+ } catch {
66
+ return false
67
+ }
68
+ }
69
+
70
+ /** Run as thin MCP proxy — forward all MCP calls to daemon via HTTP */
71
+ async function runAsProxy(daemonPort: number) {
72
+ const config = await loadConfig()
73
+ const logger = createLogger(config.logLevel, config.logFilePath)
74
+ const mainLogger = createComponentLogger(logger, 'proxy')
75
+
76
+ mainLogger.info({ daemonPort }, 'Starting as MCP proxy → daemon')
77
+
78
+ const { McpProxyServer } = await import('@/server/mcp-proxy')
79
+
80
+ let isStopping = false
81
+ const proxy = new McpProxyServer({
82
+ daemonUrl: `http://localhost:${daemonPort}`,
83
+ serverName: config.serverName,
84
+ serverVersion: config.serverVersion,
85
+ onStdinClose: () => {
86
+ if (isStopping) return
87
+ isStopping = true
88
+ mainLogger.info('stdin closed — proxy exiting')
89
+ proxy.stop().finally(() => process.exit(0))
90
+ },
91
+ })
92
+
93
+ const stopProxy = async (signal: string) => {
94
+ if (isStopping) return
95
+ isStopping = true
96
+ mainLogger.info({ signal }, 'Proxy shutting down')
97
+ await proxy.stop()
98
+ process.exit(0)
99
+ }
100
+
101
+ process.on('SIGTERM', () => stopProxy('SIGTERM'))
102
+ process.on('SIGINT', () => stopProxy('SIGINT'))
103
+ if (process.platform !== 'win32') {
104
+ process.on('SIGHUP', () => stopProxy('SIGHUP'))
105
+ }
106
+
107
+ await proxy.start()
108
+ mainLogger.info('MCP proxy ready — forwarding to daemon')
109
+ }
110
+
111
+ /** Run as full daemon with services, HTTP, and optionally MCP stdio */
112
+ async function runAsDaemon(httpOnly: boolean, pidManager: ServerPidManager) {
113
+ // Auto-install Claude Code hooks (idempotent, non-fatal)
114
+ try {
115
+ const { installHooks } = await import('@/hooks/installer')
116
+ const hookResult = installHooks()
117
+ if (hookResult.message !== 'Hooks already installed') {
118
+ console.error(`[claude-brain] ${hookResult.message}`)
119
+ }
120
+ } catch (error) {
121
+ console.error(`[claude-brain] Hook auto-install skipped: ${error instanceof Error ? error.message : 'unknown error'}`)
122
+ }
123
+
124
+ // Auto-install shell auto-start if not already present
125
+ try {
126
+ const { isAutoStartInstalled, installAutoStart } = await import('@/cli/auto-start')
127
+ if (!isAutoStartInstalled()) {
128
+ const result = installAutoStart()
129
+ if (result.success && result.profilePath) {
130
+ console.error(`[claude-brain] Auto-start installed in ${result.profilePath}`)
131
+ }
132
+ }
133
+ } catch (error) {
134
+ // Non-fatal — auto-start is a convenience feature
135
+ }
136
+
137
+ // Auto-install CLAUDE.md if not present
138
+ try {
139
+ const { existsSync, readFileSync, writeFileSync, mkdirSync } = await import('node:fs')
140
+ const { join, resolve, dirname } = await import('node:path')
141
+ const { homedir } = await import('node:os')
142
+ const { fileURLToPath } = await import('node:url')
143
+ const claudeMdDest = join(homedir(), '.claude', 'CLAUDE.md')
144
+ const pkgRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', '..')
145
+ const claudeMdSrc = join(pkgRoot, 'assets', 'CLAUDE.md')
146
+ if (existsSync(claudeMdSrc) && !existsSync(claudeMdDest)) {
147
+ mkdirSync(join(homedir(), '.claude'), { recursive: true })
148
+ writeFileSync(claudeMdDest, readFileSync(claudeMdSrc, 'utf-8'), 'utf-8')
149
+ console.error('[claude-brain] CLAUDE.md installed to ~/.claude/CLAUDE.md')
150
+ }
151
+ } catch {
152
+ // Non-fatal
153
+ }
154
+
155
+ const config = await loadConfig()
156
+
157
+ if (config.logLevel === 'debug' || config.logLevel === 'info') {
158
+ console.error(BANNER)
159
+ }
160
+
161
+ const logger = createLogger(config.logLevel, config.logFilePath)
162
+ const mainLogger = createComponentLogger(logger, 'main')
163
+
164
+ new GlobalErrorHandler(logger)
165
+ const cleanup = new CleanupManager(logger)
166
+
167
+ mainLogger.info({
168
+ serverName: config.serverName,
169
+ serverVersion: config.serverVersion,
170
+ vaultPath: config.vaultPath,
171
+ nodeEnv: config.nodeEnv
172
+ }, 'Claude Brain starting...')
173
+
174
+ mainLogger.info({
175
+ logLevel: config.logLevel,
176
+ enableFileWatch: config.enableFileWatch,
177
+ cacheSize: config.cacheSize
178
+ }, 'Configuration loaded')
179
+
180
+ // Only start ChromaDB if explicitly enabled in config
181
+ let chromaReady = false
182
+ if (config.chromadb?.enabled) {
183
+ mainLogger.info('ChromaDB enabled, ensuring it is available...')
184
+ const { ensureChromaRunning } = await import('@/cli/commands/chroma')
185
+ chromaReady = await ensureChromaRunning({ silent: process.env.NODE_ENV === 'production' })
186
+ mainLogger.info({ chromaReady }, chromaReady ? 'ChromaDB is ready' : 'ChromaDB not available, using SQLite fallback')
187
+ } else {
188
+ mainLogger.info('Using SQLite FTS5 storage (ChromaDB disabled)')
189
+ }
190
+
191
+ mainLogger.info('Initializing services...')
192
+ await initializeServices(config, logger)
193
+ mainLogger.info('Services initialized successfully')
194
+
195
+ // Initialize orchestrator for event-driven features
196
+ let orchestrator: Orchestrator | null = null
197
+ try {
198
+ const vault = getVaultService()
199
+ const memory = getMemoryService()
200
+
201
+ orchestrator = createOrchestrator(logger, vault, memory, {
202
+ maxConcurrentOperations: 5,
203
+ debounceMs: 5000
204
+ })
205
+
206
+ await orchestrator.initialize()
207
+ mainLogger.info('Orchestrator initialized successfully')
208
+ } catch (error) {
209
+ mainLogger.warn({ error }, 'Failed to initialize orchestrator, continuing without event-driven features')
210
+ }
211
+
212
+ const mcpServer = new ClaudeBrainMCPServer(config, logger, {
213
+ events: {
214
+ onConnectionStateChange: (state) => {
215
+ mainLogger.info({ state }, 'MCP connection state changed')
216
+ },
217
+ onError: (error) => {
218
+ mainLogger.error({ error: error.message }, 'MCP server error')
219
+ },
220
+ onRequest: (toolName, requestId) => {
221
+ pidManager.touchActivity()
222
+ mainLogger.debug({ toolName, requestId }, 'Tool request received')
223
+ },
224
+ onResponse: (toolName, requestId, durationMs) => {
225
+ mainLogger.debug({ toolName, requestId, durationMs }, 'Tool response sent')
226
+ }
227
+ }
228
+ })
229
+
230
+ cleanup.register(async () => {
231
+ await mcpServer.stop()
232
+ })
233
+
234
+ // Register orchestrator cleanup before services shutdown
235
+ if (orchestrator) {
236
+ cleanup.register(async () => {
237
+ orchestrator!.shutdown()
238
+ mainLogger.info('Orchestrator shut down')
239
+ })
240
+ }
241
+
242
+ cleanup.register(async () => {
243
+ await shutdownServices()
244
+ })
245
+
246
+ // ── Idempotent shutdown guard ────────────────────────────
247
+ let isShuttingDown = false
248
+ const shutdown = async (signal: string) => {
249
+ if (isShuttingDown) return
250
+ isShuttingDown = true
251
+ mainLogger.info({ signal }, 'Shutting down...')
252
+ await cleanup.cleanup()
253
+ process.exit(0)
254
+ }
255
+
256
+ // ── Primary daemon: HTTP server + background services ────
257
+ // Write PID file and register cleanup
258
+ pidManager.writePidFile(config.port)
259
+ pidManager.registerCleanupHandlers()
260
+
261
+ cleanup.register(async () => {
262
+ pidManager.cleanup()
263
+ })
264
+
265
+ // Start HTTP API server
266
+ const { HttpApiServer } = await import('@/server/http-api')
267
+ const httpServer = new HttpApiServer(config, logger)
268
+
269
+ // Wire activity tracker to PID manager for idle watchdog
270
+ httpServer.setActivityTracker(() => pidManager.touchActivity())
271
+
272
+ cleanup.register(async () => {
273
+ await httpServer.stop()
274
+ })
275
+
276
+ // Phase 21: Use session tracker from services
277
+ {
278
+ const { getSessionTracker } = await import('@/server/services')
279
+ const sessionTracker = getSessionTracker()
280
+ if (sessionTracker) {
281
+ httpServer.setSessionTracker(sessionTracker)
282
+ mainLogger.info('Session tracker wired to HTTP server')
283
+ }
284
+ }
285
+
286
+ // Phase 28: Wire code intelligence to HTTP server
287
+ {
288
+ const { getCodeIndexer, getCodeQuery } = await import('@/server/services')
289
+ const codeIndexer = getCodeIndexer()
290
+ const codeQuery = getCodeQuery()
291
+ if (codeIndexer && codeQuery) {
292
+ httpServer.setCodeIntelligence(codeIndexer, codeQuery)
293
+ mainLogger.info('Code intelligence wired to HTTP server')
294
+ }
295
+ }
296
+
297
+ // Phase 29: Wire code linker to HTTP server
298
+ {
299
+ const { getCodeLinker } = await import('@/server/services')
300
+ const codeLinker = getCodeLinker()
301
+ if (codeLinker) {
302
+ httpServer.setCodeLinker(codeLinker)
303
+ mainLogger.info('Code linker wired to HTTP server')
304
+ }
305
+ }
306
+
307
+ // Phase 30: Activity log pruning on startup + periodic cleanup
308
+ {
309
+ const memory = getMemoryService()
310
+ if (memory?.isInitialized()) {
311
+ try {
312
+ const { startPeriodicPruning } = await import('@/memory/pruning')
313
+ const db = memory.database.getDb()
314
+ const stopPruning = startPeriodicPruning(db, logger, 30)
315
+ cleanup.register(async () => {
316
+ stopPruning()
317
+ mainLogger.info('Activity log pruning stopped')
318
+ })
319
+ mainLogger.info('Activity log pruning initialized')
320
+ } catch (error) {
321
+ mainLogger.warn({ error }, 'Failed to initialize activity log pruning')
322
+ }
323
+ }
324
+ }
325
+
326
+ // Phase 30: Optional LLM compression
327
+ if (config.compression?.enabled) {
328
+ try {
329
+ const { ObservationCompressor } = await import('@/memory/compression')
330
+ const { getBrainRouter } = await import('@/routing/router')
331
+ const compressor = new ObservationCompressor(config.compression, logger)
332
+ const router = getBrainRouter(logger)
333
+ router.setCompressor(compressor)
334
+ mainLogger.info({ provider: config.compression.provider, model: config.compression.model }, 'LLM compression enabled')
335
+ } catch (error) {
336
+ mainLogger.warn({ error }, 'Failed to initialize LLM compression')
337
+ }
338
+ }
339
+
340
+ // Phase 31: Auto-update checker
341
+ let autoUpdater: InstanceType<typeof import('@/server/auto-updater').AutoUpdater> | null = null
342
+ if (config.autoUpdate?.enabled !== false) {
343
+ try {
344
+ const { AutoUpdater } = await import('@/server/auto-updater')
345
+ autoUpdater = new AutoUpdater(
346
+ {
347
+ enabled: config.autoUpdate?.enabled ?? true,
348
+ checkIntervalHours: config.autoUpdate?.checkIntervalHours ?? 24,
349
+ autoRestart: config.autoUpdate?.autoRestart ?? true,
350
+ },
351
+ mainLogger
352
+ )
353
+
354
+ // Check for updates on startup (non-blocking)
355
+ autoUpdater.check().then(result => {
356
+ if (result.updateAvailable && result.latestVersion) {
357
+ mainLogger.info(
358
+ { current: result.currentVersion, latest: result.latestVersion },
359
+ 'Update available! Run: claude-brain update'
360
+ )
361
+ }
362
+ }).catch((error) => {
363
+ mainLogger.warn({ error }, 'Background auto-update check failed')
364
+ })
365
+
366
+ // Schedule periodic checks
367
+ autoUpdater.schedulePeriodicCheck()
368
+
369
+ cleanup.register(async () => {
370
+ autoUpdater?.stopPeriodicCheck()
371
+ mainLogger.info('Auto-updater stopped')
372
+ })
373
+
374
+ mainLogger.info('Auto-updater initialized')
375
+ } catch (error) {
376
+ mainLogger.warn({ error }, 'Failed to initialize auto-updater')
377
+ }
378
+ }
379
+
380
+ // Start HTTP server after MCP server is ready
381
+ setTimeout(async () => {
382
+ try {
383
+ await httpServer.start()
384
+ mainLogger.info({ port: config.port }, 'HTTP API server started')
385
+
386
+ // Drain hook queue after HTTP server is ready
387
+ try {
388
+ const { drainQueue } = await import('@/hooks/queue')
389
+ const drained = await drainQueue(config.port)
390
+ if (drained > 0) {
391
+ mainLogger.info({ drained }, 'Drained hook queue')
392
+ }
393
+ } catch (error) {
394
+ mainLogger.debug({ error }, 'No hook queue to drain')
395
+ }
396
+ } catch (error: unknown) {
397
+ // EADDRINUSE: kill the stale process and retry once
398
+ const errObj = error as Record<string, unknown>
399
+ if (errObj?.code === 'EADDRINUSE' || String(error).includes('EADDRINUSE')) {
400
+ mainLogger.warn({ port: config.port }, 'Port in use — killing stale process and retrying')
401
+ try {
402
+ const { killProcessOnPort } = await import('@/utils/kill-port')
403
+ const killed = killProcessOnPort(config.port)
404
+ if (killed.length > 0) {
405
+ mainLogger.info({ killed }, 'Killed stale process(es) on port')
406
+ }
407
+ await new Promise(r => setTimeout(r, 1000))
408
+ await httpServer.start()
409
+ mainLogger.info({ port: config.port }, 'HTTP API server started (after recovery)')
410
+
411
+ // Drain hook queue on retry success too
412
+ try {
413
+ const { drainQueue } = await import('@/hooks/queue')
414
+ const drained = await drainQueue(config.port)
415
+ if (drained > 0) {
416
+ mainLogger.info({ drained }, 'Drained hook queue')
417
+ }
418
+ } catch {}
419
+ } catch (retryError) {
420
+ mainLogger.error({ error: retryError }, 'Failed to start HTTP API server after recovery — MCP stdio still works')
421
+ }
422
+ } else {
423
+ mainLogger.error({ error }, 'Failed to start HTTP API server')
424
+ }
425
+ }
426
+ }, 2000)
427
+
428
+ // ── Signal handlers ──────────────────────────────────────
429
+ process.on('SIGTERM', () => shutdown('SIGTERM'))
430
+ process.on('SIGINT', () => shutdown('SIGINT'))
431
+ if (process.platform !== 'win32') {
432
+ process.on('SIGHUP', () => shutdown('SIGHUP'))
433
+ }
434
+
435
+ if (httpOnly) {
436
+ // HTTP-only daemon mode: no MCP stdio. Use idle watchdog instead of infinite keepAlive.
437
+ mainLogger.info('HTTP-only mode — MCP stdio disabled (background daemon)')
438
+
439
+ const idleTimeoutMs = (config.daemon?.idleTimeoutMinutes ?? 30) * 60 * 1000
440
+ if (idleTimeoutMs > 0) {
441
+ const watchdog = setInterval(() => {
442
+ const idleMs = pidManager.getIdleMs()
443
+ if (idleMs >= idleTimeoutMs) {
444
+ mainLogger.info({ idleMs, idleTimeoutMs }, 'Idle timeout reached — shutting down daemon')
445
+ clearInterval(watchdog)
446
+ shutdown('idle-timeout')
447
+ }
448
+ }, 60_000)
449
+ cleanup.register(async () => clearInterval(watchdog))
450
+ mainLogger.info({ idleTimeoutMinutes: config.daemon?.idleTimeoutMinutes ?? 30 }, 'Idle watchdog started')
451
+ } else {
452
+ // idleTimeoutMinutes=0 means never self-terminate (old behavior)
453
+ const keepAlive = setInterval(() => {}, 60_000 * 60)
454
+ cleanup.register(async () => clearInterval(keepAlive))
455
+ mainLogger.info('Idle watchdog disabled (idleTimeoutMinutes=0)')
456
+ }
457
+ } else {
458
+ // MCP stdio mode: start MCP server and detect stdin closure
459
+ await mcpServer.start()
460
+ mainLogger.info('Claude Brain MCP server ready and listening for connections')
461
+
462
+ // Detect stdin close — Claude Code disconnected
463
+ process.stdin.on('end', () => shutdown('stdin-end'))
464
+ process.stdin.on('close', () => shutdown('stdin-close'))
465
+ }
466
+ }