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.
- package/README.md +241 -191
- package/VERSION +1 -1
- package/assets/CLAUDE-unified.md +11 -11
- package/assets/CLAUDE.md +29 -29
- package/package.json +7 -3
- package/packs/backend/node.json +173 -173
- package/packs/core/javascript.json +176 -176
- package/packs/core/typescript.json +222 -222
- package/packs/frontend/react.json +254 -254
- package/packs/meta/testing.json +172 -172
- package/scripts/postinstall.mjs +531 -531
- package/src/automation/decision-detector.ts +452 -452
- package/src/automation/phase12-manager.ts +456 -456
- package/src/automation/proactive-recall.ts +373 -373
- package/src/automation/project-detector.ts +310 -310
- package/src/automation/repo-scanner.ts +210 -205
- package/src/cli/auto-setup.ts +75 -75
- package/src/cli/auto-start.ts +266 -266
- package/src/cli/bin.ts +264 -264
- package/src/cli/commands/autostart.ts +90 -90
- package/src/cli/commands/chroma.ts +578 -577
- package/src/cli/commands/export-training.ts +70 -70
- package/src/cli/commands/export.ts +130 -130
- package/src/cli/commands/git-hook.ts +183 -183
- package/src/cli/commands/hooks.ts +217 -217
- package/src/cli/commands/init.ts +123 -123
- package/src/cli/commands/install-mcp.ts +122 -111
- package/src/cli/commands/models.ts +979 -979
- package/src/cli/commands/pack.ts +200 -200
- package/src/cli/commands/refresh.ts +344 -339
- package/src/cli/commands/reindex.ts +120 -120
- package/src/cli/commands/serve.ts +466 -463
- package/src/cli/commands/start.ts +44 -44
- package/src/cli/commands/status.ts +220 -203
- package/src/cli/commands/uninstall-mcp.ts +45 -41
- package/src/cli/commands/update.ts +130 -124
- package/src/cli/migrate-chroma.ts +106 -106
- package/src/cli/ui/animations.ts +80 -80
- package/src/cli/ui/components.ts +82 -82
- package/src/cli/ui/index.ts +4 -4
- package/src/cli/ui/logo.ts +36 -36
- package/src/cli/ui/theme.ts +55 -55
- package/src/code-intelligence/indexer.ts +352 -352
- package/src/code-intelligence/linker.ts +178 -178
- package/src/code-intelligence/parser.ts +484 -484
- package/src/code-intelligence/query.ts +291 -291
- package/src/code-intelligence/schema.ts +83 -83
- package/src/code-intelligence/types.ts +95 -95
- package/src/config/defaults.ts +52 -52
- package/src/config/home.ts +56 -56
- package/src/config/index.ts +5 -5
- package/src/config/loader.ts +192 -192
- package/src/config/schema.ts +446 -415
- package/src/config/validator.ts +182 -182
- package/src/context/assembler.ts +407 -400
- package/src/context/index.ts +79 -79
- package/src/context/progress-tracker.ts +174 -174
- package/src/context/standards-manager.ts +287 -287
- package/src/context/validator.ts +58 -58
- package/src/diagnostics/index.ts +122 -121
- package/src/health/index.ts +233 -232
- package/src/hooks/brain-hook.ts +134 -131
- package/src/hooks/capture.ts +168 -168
- package/src/hooks/claude-code-mastery.md +112 -112
- package/src/hooks/context-hook.ts +260 -245
- package/src/hooks/deduplicator.ts +72 -72
- package/src/hooks/git-capture.ts +109 -109
- package/src/hooks/git-hook-installer.ts +211 -207
- package/src/hooks/index.ts +20 -20
- package/src/hooks/installer.ts +306 -288
- package/src/hooks/interceptor-hook.ts +204 -201
- package/src/hooks/passive-classifier.ts +397 -397
- package/src/hooks/queue.ts +160 -129
- package/src/hooks/session-tracker.ts +312 -312
- package/src/hooks/types.ts +52 -52
- package/src/index.ts +7 -7
- package/src/intelligence/cross-project/generalizer.ts +283 -283
- package/src/intelligence/cross-project/index.ts +7 -7
- package/src/intelligence/hf-downloader.ts +222 -222
- package/src/intelligence/hf-manifest.json +78 -78
- package/src/intelligence/index.ts +24 -24
- package/src/intelligence/inference-router.ts +762 -762
- package/src/intelligence/model-manager.ts +263 -245
- package/src/intelligence/optimization/index.ts +10 -10
- package/src/intelligence/optimization/precompute.ts +202 -202
- package/src/intelligence/optimization/semantic-cache.ts +213 -207
- package/src/intelligence/prediction/index.ts +7 -7
- package/src/intelligence/prediction/recommender.ts +276 -268
- package/src/intelligence/reasoning/chain-retrieval.ts +243 -247
- package/src/intelligence/reasoning/index.ts +7 -7
- package/src/intelligence/temporal/evolution.ts +193 -197
- package/src/intelligence/temporal/index.ts +16 -16
- package/src/intelligence/temporal/query-processor.ts +190 -190
- package/src/intelligence/temporal/timeline.ts +272 -259
- package/src/intelligence/temporal/trends.ts +263 -263
- package/src/intelligence/tokenizer.ts +118 -118
- package/src/knowledge/entity-extractor.ts +447 -443
- package/src/knowledge/graph/builder.ts +185 -185
- package/src/knowledge/graph/linker.ts +201 -201
- package/src/knowledge/graph/memory-graph.ts +359 -359
- package/src/knowledge/graph/schema.ts +99 -99
- package/src/knowledge/graph/search.ts +166 -166
- package/src/knowledge/relationship-extractor.ts +108 -108
- package/src/memory/chroma/client.ts +211 -192
- package/src/memory/chroma/collection-manager.ts +92 -92
- package/src/memory/chroma/config.ts +57 -57
- package/src/memory/chroma/embeddings.ts +177 -175
- package/src/memory/chroma/index.ts +82 -82
- package/src/memory/chroma/migration.ts +270 -270
- package/src/memory/chroma/schemas.ts +69 -69
- package/src/memory/chroma/search.ts +319 -315
- package/src/memory/chroma/store.ts +755 -747
- package/src/memory/compression.ts +121 -121
- package/src/memory/consolidation/archiver.ts +162 -165
- package/src/memory/consolidation/merger.ts +182 -186
- package/src/memory/consolidation/scorer.ts +136 -136
- package/src/memory/database.ts +9 -0
- package/src/memory/dual-write.ts +145 -0
- package/src/memory/embeddings.ts +226 -226
- package/src/memory/episodic/detector.ts +108 -108
- package/src/memory/episodic/manager.ts +347 -351
- package/src/memory/episodic/summarizer.ts +179 -179
- package/src/memory/episodic/types.ts +52 -52
- package/src/memory/fts5-search.ts +692 -633
- package/src/memory/index.ts +943 -1060
- package/src/memory/migrations/add-fts5.ts +118 -108
- package/src/memory/patterns.ts +438 -438
- package/src/memory/pruning.ts +60 -60
- package/src/memory/schema.ts +88 -88
- package/src/memory/store.ts +911 -787
- package/src/orchestrator/handlers/decision-handler.ts +204 -204
- package/src/packs/index.ts +9 -9
- package/src/packs/loader.ts +134 -134
- package/src/packs/manager.ts +204 -204
- package/src/packs/ranker.ts +78 -78
- package/src/packs/types.ts +81 -81
- package/src/phase12/index.ts +5 -5
- package/src/retrieval/bm25/index.ts +300 -297
- package/src/retrieval/bm25/tokenizer.ts +184 -184
- package/src/retrieval/feedback/adaptive.ts +221 -221
- package/src/retrieval/feedback/index.ts +16 -16
- package/src/retrieval/feedback/metrics.ts +221 -221
- package/src/retrieval/feedback/store.ts +283 -283
- package/src/retrieval/fusion/index.ts +194 -194
- package/src/retrieval/fusion/rrf.ts +165 -165
- package/src/retrieval/index.ts +12 -12
- package/src/retrieval/pipeline.ts +375 -375
- package/src/retrieval/query/expander.ts +203 -203
- package/src/retrieval/query/index.ts +27 -27
- package/src/retrieval/query/intent-classifier.ts +252 -252
- package/src/retrieval/query/temporal-parser.ts +295 -295
- package/src/retrieval/reranker/index.ts +189 -188
- package/src/retrieval/reranker/model.ts +99 -95
- package/src/retrieval/service.ts +125 -125
- package/src/retrieval/types.ts +162 -162
- package/src/routing/entity-extractor.ts +454 -454
- package/src/routing/handlers/exploration-handler.ts +369 -0
- package/src/routing/handlers/index.ts +19 -0
- package/src/routing/handlers/memory-handler.ts +273 -0
- package/src/routing/handlers/mutation-handler.ts +241 -0
- package/src/routing/handlers/recall-handler.ts +642 -0
- package/src/routing/handlers/shared.ts +515 -0
- package/src/routing/handlers/types.ts +48 -0
- package/src/routing/intent-classifier.ts +552 -552
- package/src/routing/response-filter.ts +399 -391
- package/src/routing/router.ts +245 -2193
- package/src/routing/search-engine.ts +521 -514
- package/src/routing/types.ts +104 -94
- package/src/scripts/health-check.ts +118 -118
- package/src/scripts/setup.ts +122 -122
- package/src/server/auto-updater.ts +283 -276
- package/src/server/handlers/call-tool.ts +159 -159
- package/src/server/handlers/list-tools.ts +35 -35
- package/src/server/handlers/tools/auto-remember.ts +165 -165
- package/src/server/handlers/tools/brain.ts +86 -86
- package/src/server/handlers/tools/create-project.ts +135 -135
- package/src/server/handlers/tools/get-code-standards.ts +123 -123
- package/src/server/handlers/tools/get-corrections.ts +152 -152
- package/src/server/handlers/tools/get-patterns.ts +156 -156
- package/src/server/handlers/tools/get-project-context.ts +75 -75
- package/src/server/handlers/tools/index.ts +30 -30
- package/src/server/handlers/tools/init-project.ts +756 -756
- package/src/server/handlers/tools/list-projects.ts +126 -126
- package/src/server/handlers/tools/recall-similar.ts +87 -87
- package/src/server/handlers/tools/recognize-pattern.ts +132 -132
- package/src/server/handlers/tools/record-correction.ts +131 -131
- package/src/server/handlers/tools/remember-decision.ts +168 -168
- package/src/server/handlers/tools/schemas.ts +179 -179
- package/src/server/handlers/tools/search-code.ts +122 -122
- package/src/server/handlers/tools/smart-context.ts +146 -146
- package/src/server/handlers/tools/update-progress.ts +131 -131
- package/src/server/http-api.ts +215 -1229
- package/src/server/mcp-proxy.ts +85 -84
- package/src/server/mcp-server.ts +285 -284
- package/src/server/middleware/auth.ts +39 -0
- package/src/server/middleware/error-handler.ts +37 -0
- package/src/server/middleware/rate-limit.ts +53 -0
- package/src/server/middleware/validate.ts +42 -0
- package/src/server/pid-manager.ts +137 -136
- package/src/server/providers/resources.ts +581 -581
- package/src/server/routes/code.ts +228 -0
- package/src/server/routes/context.ts +26 -0
- package/src/server/routes/health.ts +19 -0
- package/src/server/routes/helpers.ts +100 -0
- package/src/server/routes/hooks.ts +197 -0
- package/src/server/routes/mcp.ts +47 -0
- package/src/server/routes/memory.ts +397 -0
- package/src/server/routes/models.ts +96 -0
- package/src/server/routes/projects.ts +89 -0
- package/src/server/routes/types.ts +21 -0
- package/src/server/schemas/api-schemas.ts +202 -0
- package/src/server/services.ts +720 -720
- package/src/server/utils/memory-indicator.ts +84 -84
- package/src/server/utils/response-formatter.ts +129 -129
- package/src/server/web-viewer.ts +1145 -1115
- package/src/setup/index.ts +38 -38
- package/src/tools/registry.ts +115 -115
- package/src/tools/schemas.ts +666 -666
- package/src/tools/types.ts +412 -412
- package/src/training/data-store.ts +320 -298
- package/src/training/retrain-pipeline.ts +399 -394
- package/src/utils/error-handler.ts +136 -136
- package/src/utils/index.ts +58 -58
- package/src/utils/kill-port.ts +55 -53
- package/src/utils/phase12-helper.ts +56 -56
- package/src/utils/safe-path.ts +43 -0
- package/src/utils/timing.ts +47 -47
- package/src/utils/transaction.ts +63 -63
- package/src/vault/index.ts +4 -3
- package/src/vault/paths.ts +106 -106
- package/src/vault/query.ts +4 -1
- package/src/vault/reader.ts +44 -1
- package/src/vault/watcher.ts +24 -1
- package/src/vault/writer.ts +487 -413
- package/skills/persistent-memory/SKILL.md +0 -148
- 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
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
mainLogger.
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|
+
}
|