claude-brain 0.24.0 → 0.24.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.24.0
1
+ 0.24.2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-brain",
3
- "version": "0.24.0",
3
+ "version": "0.24.2",
4
4
  "description": "Local development assistant bridging Obsidian vaults with Claude Code via MCP",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -342,7 +342,7 @@ function buildAutoStartSnippet(profilePath) {
342
342
  'if (Get-Command claude-brain -ErrorAction SilentlyContinue) {',
343
343
  ' $listening = Get-NetTCPConnection -LocalPort 3000 -ErrorAction SilentlyContinue',
344
344
  ' if (-not $listening) {',
345
- ' Start-Process -NoNewWindow -FilePath "claude-brain" -ArgumentList "serve" -WindowStyle Hidden',
345
+ ' Start-Process -NoNewWindow -FilePath "claude-brain" -ArgumentList "serve","--http-only" -WindowStyle Hidden',
346
346
  ' }',
347
347
  '}',
348
348
  AUTO_END_MARKER,
@@ -350,9 +350,9 @@ function buildAutoStartSnippet(profilePath) {
350
350
  }
351
351
  return [
352
352
  AUTO_START_MARKER,
353
- '# Auto-start claude-brain server if not already running',
353
+ '# Auto-start claude-brain HTTP server if not already running',
354
354
  '(command -v claude-brain >/dev/null 2>&1 && ! lsof -ti :3000 >/dev/null 2>&1) && {',
355
- ' nohup claude-brain serve >/dev/null 2>&1 &',
355
+ ' nohup claude-brain serve --http-only >/dev/null 2>&1 &',
356
356
  ' disown 2>/dev/null',
357
357
  '}',
358
358
  AUTO_END_MARKER,
@@ -32,9 +32,9 @@ export interface AutoStartResult {
32
32
  function buildUnixSnippet(port: number): string {
33
33
  return [
34
34
  START_MARKER,
35
- '# Auto-start claude-brain server if not already running',
35
+ '# Auto-start claude-brain HTTP server if not already running',
36
36
  `(command -v claude-brain >/dev/null 2>&1 && ! lsof -ti :${port} >/dev/null 2>&1) && {`,
37
- ' nohup claude-brain serve >/dev/null 2>&1 &',
37
+ ' nohup claude-brain serve --http-only >/dev/null 2>&1 &',
38
38
  ' disown 2>/dev/null',
39
39
  '}',
40
40
  END_MARKER,
@@ -48,9 +48,9 @@ function buildUnixSnippet(port: number): string {
48
48
  function buildFishSnippet(port: number): string {
49
49
  return [
50
50
  START_MARKER,
51
- '# Auto-start claude-brain server if not already running',
51
+ '# Auto-start claude-brain HTTP server if not already running',
52
52
  `if command -v claude-brain >/dev/null 2>&1; and not lsof -ti :${port} >/dev/null 2>&1`,
53
- ' nohup claude-brain serve >/dev/null 2>&1 &',
53
+ ' nohup claude-brain serve --http-only >/dev/null 2>&1 &',
54
54
  'end',
55
55
  END_MARKER,
56
56
  ].join('\n')
@@ -66,7 +66,7 @@ function buildPowerShellSnippet(port: number): string {
66
66
  ` $port = ${port}`,
67
67
  ' $listening = Get-NetTCPConnection -LocalPort $port -ErrorAction SilentlyContinue',
68
68
  ' if (-not $listening) {',
69
- ' Start-Process -NoNewWindow -FilePath "claude-brain" -ArgumentList "serve" -WindowStyle Hidden',
69
+ ' Start-Process -NoNewWindow -FilePath "claude-brain" -ArgumentList "serve","--http-only" -WindowStyle Hidden',
70
70
  ' }',
71
71
  '}',
72
72
  END_MARKER,
@@ -18,17 +18,22 @@ export async function runServe() {
18
18
  // Auto-initialize home directory on first run
19
19
  ensureHomeDirectory()
20
20
 
21
- // Singleton check: prevent multiple server instances
21
+ // --http-only: run HTTP server + services but no MCP stdio transport.
22
+ // Used by auto-start background process (stdin is /dev/null, no Claude Code attached).
23
+ const httpOnly = process.argv.includes('--http-only')
24
+
25
+ // Determine instance role:
26
+ // - httpOnly → always primary (background HTTP daemon)
27
+ // - otherwise → secondary if primary is already running (Claude Code MCP session)
22
28
  const pidManager = new ServerPidManager()
23
29
  const existingPid = pidManager.getRunningPid()
24
- if (existingPid) {
25
- console.error(`[claude-brain] Server already running (PID: ${existingPid}). Exiting.`)
26
- process.exit(0)
27
- }
30
+ const isSecondaryInstance = !httpOnly && !!existingPid
28
31
 
29
- // Write PID file and register cleanup handlers
30
- pidManager.writePidFile()
31
- pidManager.registerCleanupHandlers()
32
+ if (!isSecondaryInstance) {
33
+ // Primary instance: write PID file and register cleanup
34
+ pidManager.writePidFile()
35
+ pidManager.registerCleanupHandlers()
36
+ }
32
37
 
33
38
  // Auto-install Claude Code hooks (idempotent, non-fatal)
34
39
  try {
@@ -41,6 +46,37 @@ export async function runServe() {
41
46
  console.error(`[claude-brain] Hook auto-install skipped: ${error instanceof Error ? error.message : 'unknown error'}`)
42
47
  }
43
48
 
49
+ // Auto-install shell auto-start if not already present (handles bun blocking postinstall)
50
+ try {
51
+ const { isAutoStartInstalled, installAutoStart } = await import('@/cli/auto-start')
52
+ if (!isAutoStartInstalled()) {
53
+ const result = installAutoStart()
54
+ if (result.success && result.profilePath) {
55
+ console.error(`[claude-brain] Auto-start installed in ${result.profilePath}`)
56
+ }
57
+ }
58
+ } catch (error) {
59
+ // Non-fatal — auto-start is a convenience feature
60
+ }
61
+
62
+ // Auto-install CLAUDE.md if not present (handles bun blocking postinstall)
63
+ try {
64
+ const { existsSync, readFileSync, writeFileSync, mkdirSync } = await import('node:fs')
65
+ const { join, resolve, dirname } = await import('node:path')
66
+ const { homedir } = await import('node:os')
67
+ const { fileURLToPath } = await import('node:url')
68
+ const claudeMdDest = join(homedir(), '.claude', 'CLAUDE.md')
69
+ const pkgRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', '..')
70
+ const claudeMdSrc = join(pkgRoot, 'assets', 'CLAUDE.md')
71
+ if (existsSync(claudeMdSrc) && !existsSync(claudeMdDest)) {
72
+ mkdirSync(join(homedir(), '.claude'), { recursive: true })
73
+ writeFileSync(claudeMdDest, readFileSync(claudeMdSrc, 'utf-8'), 'utf-8')
74
+ console.error('[claude-brain] CLAUDE.md installed to ~/.claude/CLAUDE.md')
75
+ }
76
+ } catch {
77
+ // Non-fatal
78
+ }
79
+
44
80
  const config = await loadConfig()
45
81
 
46
82
  if (config.logLevel === 'debug' || config.logLevel === 'info') {
@@ -131,141 +167,147 @@ export async function runServe() {
131
167
  await shutdownServices()
132
168
  })
133
169
 
134
- // Clean up PID file during graceful shutdown
135
- cleanup.register(async () => {
136
- pidManager.cleanup()
137
- })
170
+ // ── Primary instance only: HTTP server + background services ──
171
+ // Secondary instances (when auto-start already running) only start MCP stdio
172
+ if (!isSecondaryInstance) {
173
+ // Clean up PID file during graceful shutdown
174
+ cleanup.register(async () => {
175
+ pidManager.cleanup()
176
+ })
138
177
 
139
- // Start HTTP API server alongside MCP server
140
- const { HttpApiServer } = await import('@/server/http-api')
141
- const httpServer = new HttpApiServer(config, logger)
178
+ // Start HTTP API server alongside MCP server
179
+ const { HttpApiServer } = await import('@/server/http-api')
180
+ const httpServer = new HttpApiServer(config, logger)
142
181
 
143
- cleanup.register(async () => {
144
- await httpServer.stop()
145
- })
182
+ cleanup.register(async () => {
183
+ await httpServer.stop()
184
+ })
146
185
 
147
- // Phase 21: Use session tracker from services (promoted from local creation)
148
- {
149
- const { getSessionTracker } = await import('@/server/services')
150
- const sessionTracker = getSessionTracker()
151
- if (sessionTracker) {
152
- httpServer.setSessionTracker(sessionTracker)
153
- mainLogger.info('Session tracker wired to HTTP server')
186
+ // Phase 21: Use session tracker from services (promoted from local creation)
187
+ {
188
+ const { getSessionTracker } = await import('@/server/services')
189
+ const sessionTracker = getSessionTracker()
190
+ if (sessionTracker) {
191
+ httpServer.setSessionTracker(sessionTracker)
192
+ mainLogger.info('Session tracker wired to HTTP server')
193
+ }
154
194
  }
155
- }
156
195
 
157
- // Phase 28: Wire code intelligence to HTTP server
158
- {
159
- const { getCodeIndexer, getCodeQuery } = await import('@/server/services')
160
- const codeIndexer = getCodeIndexer()
161
- const codeQuery = getCodeQuery()
162
- if (codeIndexer && codeQuery) {
163
- httpServer.setCodeIntelligence(codeIndexer, codeQuery)
164
- mainLogger.info('Code intelligence wired to HTTP server')
196
+ // Phase 28: Wire code intelligence to HTTP server
197
+ {
198
+ const { getCodeIndexer, getCodeQuery } = await import('@/server/services')
199
+ const codeIndexer = getCodeIndexer()
200
+ const codeQuery = getCodeQuery()
201
+ if (codeIndexer && codeQuery) {
202
+ httpServer.setCodeIntelligence(codeIndexer, codeQuery)
203
+ mainLogger.info('Code intelligence wired to HTTP server')
204
+ }
205
+ }
206
+
207
+ // Phase 29: Wire code linker to HTTP server
208
+ {
209
+ const { getCodeLinker } = await import('@/server/services')
210
+ const codeLinker = getCodeLinker()
211
+ if (codeLinker) {
212
+ httpServer.setCodeLinker(codeLinker)
213
+ mainLogger.info('Code linker wired to HTTP server')
214
+ }
165
215
  }
166
- }
167
216
 
168
- // Phase 29: Wire code linker to HTTP server
169
- {
170
- const { getCodeLinker } = await import('@/server/services')
171
- const codeLinker = getCodeLinker()
172
- if (codeLinker) {
173
- httpServer.setCodeLinker(codeLinker)
174
- mainLogger.info('Code linker wired to HTTP server')
217
+ // Phase 30: Activity log pruning on startup + periodic cleanup
218
+ {
219
+ const memory = getMemoryService()
220
+ if (memory?.isInitialized()) {
221
+ try {
222
+ const { startPeriodicPruning } = await import('@/memory/pruning')
223
+ const db = memory.database.getDb()
224
+ const stopPruning = startPeriodicPruning(db, logger, 30)
225
+ cleanup.register(async () => {
226
+ stopPruning()
227
+ mainLogger.info('Activity log pruning stopped')
228
+ })
229
+ mainLogger.info('Activity log pruning initialized')
230
+ } catch (error) {
231
+ mainLogger.warn({ error }, 'Failed to initialize activity log pruning')
232
+ }
233
+ }
175
234
  }
176
- }
177
235
 
178
- // Phase 30: Activity log pruning on startup + periodic cleanup
179
- {
180
- const memory = getMemoryService()
181
- if (memory?.isInitialized()) {
236
+ // Phase 30: Optional LLM compression
237
+ if (config.compression?.enabled) {
182
238
  try {
183
- const { startPeriodicPruning } = await import('@/memory/pruning')
184
- const db = memory.database.getDb()
185
- const stopPruning = startPeriodicPruning(db, logger, 30)
186
- cleanup.register(async () => {
187
- stopPruning()
188
- mainLogger.info('Activity log pruning stopped')
189
- })
190
- mainLogger.info('Activity log pruning initialized')
239
+ const { ObservationCompressor } = await import('@/memory/compression')
240
+ const { getBrainRouter } = await import('@/routing/router')
241
+ const compressor = new ObservationCompressor(config.compression, logger)
242
+ const router = getBrainRouter(logger)
243
+ router.setCompressor(compressor)
244
+ mainLogger.info({ provider: config.compression.provider, model: config.compression.model }, 'LLM compression enabled')
191
245
  } catch (error) {
192
- mainLogger.warn({ error }, 'Failed to initialize activity log pruning')
246
+ mainLogger.warn({ error }, 'Failed to initialize LLM compression')
193
247
  }
194
248
  }
195
- }
196
-
197
- // Phase 30: Optional LLM compression
198
- if (config.compression?.enabled) {
199
- try {
200
- const { ObservationCompressor } = await import('@/memory/compression')
201
- const { getBrainRouter } = await import('@/routing/router')
202
- const compressor = new ObservationCompressor(config.compression, logger)
203
- const router = getBrainRouter(logger)
204
- router.setCompressor(compressor)
205
- mainLogger.info({ provider: config.compression.provider, model: config.compression.model }, 'LLM compression enabled')
206
- } catch (error) {
207
- mainLogger.warn({ error }, 'Failed to initialize LLM compression')
208
- }
209
- }
210
-
211
- // Phase 31: Auto-update checker
212
- let autoUpdater: InstanceType<typeof import('@/server/auto-updater').AutoUpdater> | null = null
213
- if (config.autoUpdate?.enabled !== false) {
214
- try {
215
- const { AutoUpdater } = await import('@/server/auto-updater')
216
- autoUpdater = new AutoUpdater(
217
- {
218
- enabled: config.autoUpdate?.enabled ?? true,
219
- checkIntervalHours: config.autoUpdate?.checkIntervalHours ?? 24,
220
- autoRestart: config.autoUpdate?.autoRestart ?? true,
221
- },
222
- mainLogger
223
- )
224
-
225
- // Check for updates on startup (non-blocking)
226
- autoUpdater.check().then(result => {
227
- if (result.updateAvailable && result.latestVersion) {
228
- mainLogger.info(
229
- { current: result.currentVersion, latest: result.latestVersion },
230
- 'Update available! Run: claude-brain update'
231
- )
232
- }
233
- }).catch(() => {})
234
249
 
235
- // Schedule periodic checks
236
- autoUpdater.schedulePeriodicCheck()
250
+ // Phase 31: Auto-update checker
251
+ let autoUpdater: InstanceType<typeof import('@/server/auto-updater').AutoUpdater> | null = null
252
+ if (config.autoUpdate?.enabled !== false) {
253
+ try {
254
+ const { AutoUpdater } = await import('@/server/auto-updater')
255
+ autoUpdater = new AutoUpdater(
256
+ {
257
+ enabled: config.autoUpdate?.enabled ?? true,
258
+ checkIntervalHours: config.autoUpdate?.checkIntervalHours ?? 24,
259
+ autoRestart: config.autoUpdate?.autoRestart ?? true,
260
+ },
261
+ mainLogger
262
+ )
263
+
264
+ // Check for updates on startup (non-blocking)
265
+ autoUpdater.check().then(result => {
266
+ if (result.updateAvailable && result.latestVersion) {
267
+ mainLogger.info(
268
+ { current: result.currentVersion, latest: result.latestVersion },
269
+ 'Update available! Run: claude-brain update'
270
+ )
271
+ }
272
+ }).catch(() => {})
273
+
274
+ // Schedule periodic checks
275
+ autoUpdater.schedulePeriodicCheck()
237
276
 
238
- cleanup.register(async () => {
239
- autoUpdater?.stopPeriodicCheck()
240
- mainLogger.info('Auto-updater stopped')
241
- })
277
+ cleanup.register(async () => {
278
+ autoUpdater?.stopPeriodicCheck()
279
+ mainLogger.info('Auto-updater stopped')
280
+ })
242
281
 
243
- mainLogger.info('Auto-updater initialized')
244
- } catch (error) {
245
- mainLogger.warn({ error }, 'Failed to initialize auto-updater')
282
+ mainLogger.info('Auto-updater initialized')
283
+ } catch (error) {
284
+ mainLogger.warn({ error }, 'Failed to initialize auto-updater')
285
+ }
246
286
  }
247
- }
248
-
249
- // Start HTTP server after MCP server is ready
250
- setTimeout(async () => {
251
- try {
252
- await httpServer.start()
253
- mainLogger.info({ port: config.port }, 'HTTP API server started')
254
287
 
255
- // Drain hook queue after HTTP server is ready
288
+ // Start HTTP server after MCP server is ready
289
+ setTimeout(async () => {
256
290
  try {
257
- const { drainQueue } = await import('@/hooks/queue')
258
- const drained = await drainQueue(config.port)
259
- if (drained > 0) {
260
- mainLogger.info({ drained }, 'Drained hook queue')
291
+ await httpServer.start()
292
+ mainLogger.info({ port: config.port }, 'HTTP API server started')
293
+
294
+ // Drain hook queue after HTTP server is ready
295
+ try {
296
+ const { drainQueue } = await import('@/hooks/queue')
297
+ const drained = await drainQueue(config.port)
298
+ if (drained > 0) {
299
+ mainLogger.info({ drained }, 'Drained hook queue')
300
+ }
301
+ } catch (error) {
302
+ mainLogger.debug({ error }, 'No hook queue to drain')
261
303
  }
262
304
  } catch (error) {
263
- mainLogger.debug({ error }, 'No hook queue to drain')
305
+ mainLogger.error({ error }, 'Failed to start HTTP API server')
264
306
  }
265
- } catch (error) {
266
- mainLogger.error({ error }, 'Failed to start HTTP API server')
267
- }
268
- }, 2000)
307
+ }, 2000)
308
+ } else {
309
+ mainLogger.info({ existingPid }, 'Secondary instance — HTTP server already running, starting MCP stdio only')
310
+ }
269
311
 
270
312
  const shutdown = async (signal: string) => {
271
313
  mainLogger.info({ signal }, 'Shutting down...')
@@ -276,7 +318,14 @@ export async function runServe() {
276
318
  process.on('SIGTERM', () => shutdown('SIGTERM'))
277
319
  process.on('SIGINT', () => shutdown('SIGINT'))
278
320
 
279
- await mcpServer.start()
280
-
281
- mainLogger.info('Claude Brain MCP server ready and listening for connections')
321
+ if (httpOnly) {
322
+ // HTTP-only mode: no MCP stdio. Keep process alive for HTTP server.
323
+ mainLogger.info('HTTP-only mode MCP stdio disabled (background daemon)')
324
+ // Keep the event loop alive with a long interval
325
+ const keepAlive = setInterval(() => {}, 60_000 * 60)
326
+ cleanup.register(async () => { clearInterval(keepAlive) })
327
+ } else {
328
+ await mcpServer.start()
329
+ mainLogger.info('Claude Brain MCP server ready and listening for connections')
330
+ }
282
331
  }