claude-brain 0.22.4 → 0.24.0

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.22.4
1
+ 0.24.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-brain",
3
- "version": "0.22.4",
3
+ "version": "0.24.0",
4
4
  "description": "Local development assistant bridging Obsidian vaults with Claude Code via MCP",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -314,6 +314,80 @@ function installClaudeMd() {
314
314
  return true
315
315
  }
316
316
 
317
+ // ── Step 6: Install auto-start in shell profile ──────────
318
+
319
+ const AUTO_START_MARKER = '# >>> claude-brain auto-start >>>'
320
+ const AUTO_END_MARKER = '# <<< claude-brain auto-start <<<'
321
+
322
+ function getShellProfile() {
323
+ const home = homedir()
324
+ const os = process.platform
325
+
326
+ if (os === 'win32') {
327
+ const userProfile = process.env.USERPROFILE
328
+ if (userProfile) {
329
+ return join(userProfile, 'Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1')
330
+ }
331
+ return null
332
+ }
333
+
334
+ // macOS defaults to zsh, Linux to bash
335
+ return os === 'darwin' ? join(home, '.zshrc') : join(home, '.bashrc')
336
+ }
337
+
338
+ function buildAutoStartSnippet(profilePath) {
339
+ if (profilePath && profilePath.endsWith('.ps1')) {
340
+ return [
341
+ AUTO_START_MARKER,
342
+ 'if (Get-Command claude-brain -ErrorAction SilentlyContinue) {',
343
+ ' $listening = Get-NetTCPConnection -LocalPort 3000 -ErrorAction SilentlyContinue',
344
+ ' if (-not $listening) {',
345
+ ' Start-Process -NoNewWindow -FilePath "claude-brain" -ArgumentList "serve" -WindowStyle Hidden',
346
+ ' }',
347
+ '}',
348
+ AUTO_END_MARKER,
349
+ ].join('\n')
350
+ }
351
+ return [
352
+ AUTO_START_MARKER,
353
+ '# Auto-start claude-brain server if not already running',
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 &',
356
+ ' disown 2>/dev/null',
357
+ '}',
358
+ AUTO_END_MARKER,
359
+ ].join('\n')
360
+ }
361
+
362
+ function installShellAutoStart() {
363
+ const profilePath = getShellProfile()
364
+ if (!profilePath) {
365
+ log('No supported shell profile found, skipping auto-start')
366
+ return false
367
+ }
368
+
369
+ // Check if already installed
370
+ if (existsSync(profilePath)) {
371
+ const content = readFileSync(profilePath, 'utf-8')
372
+ if (content.includes(AUTO_START_MARKER)) {
373
+ log('Auto-start already installed')
374
+ return true
375
+ }
376
+ }
377
+
378
+ // Ensure parent directory exists
379
+ const dir = join(profilePath, '..')
380
+ mkdirSync(dir, { recursive: true })
381
+
382
+ const existing = existsSync(profilePath) ? readFileSync(profilePath, 'utf-8') : ''
383
+ const separator = existing.length > 0 && !existing.endsWith('\n') ? '\n\n' : '\n'
384
+ const snippet = buildAutoStartSnippet(profilePath)
385
+ writeFileSync(profilePath, existing + separator + snippet + '\n', 'utf-8')
386
+
387
+ log(`Auto-start installed in ${profilePath}`)
388
+ return true
389
+ }
390
+
317
391
  // ── Main ─────────────────────────────────────────────────
318
392
 
319
393
  async function main() {
@@ -332,6 +406,7 @@ async function main() {
332
406
  hooks: false,
333
407
  hookFiles: false,
334
408
  claudemd: false,
409
+ autostart: false,
335
410
  }
336
411
 
337
412
  // Step 1: Create data directory
@@ -349,15 +424,17 @@ async function main() {
349
424
  // Step 5: Install CLAUDE.md
350
425
  try { results.claudemd = installClaudeMd() } catch (e) { log(`CLAUDE.md install failed: ${e.message}`) }
351
426
 
427
+ // Step 6: Install auto-start in shell profile
428
+ try { results.autostart = installShellAutoStart() } catch (e) { log(`Auto-start install failed: ${e.message}`) }
429
+
352
430
  // Print success summary
353
431
  console.error('')
354
432
  console.error(`${PREFIX} ────────────────────────────────────────`)
355
433
  console.error(`${PREFIX}`)
356
434
  console.error(`${PREFIX} Claude Brain installed successfully!`)
357
435
  console.error(`${PREFIX}`)
358
- console.error(`${PREFIX} Next steps:`)
359
- console.error(`${PREFIX} 1. Start the server: claude-brain start`)
360
- console.error(`${PREFIX} 2. Restart Claude Code to activate hooks`)
436
+ console.error(`${PREFIX} Server will auto-start on next terminal session.`)
437
+ console.error(`${PREFIX} Auto-updates check every 24h in the background.`)
361
438
  console.error(`${PREFIX}`)
362
439
  console.error(`${PREFIX} Data: ${HOME}/data/`)
363
440
  console.error(`${PREFIX} Config: ${HOME}/config.yml`)
@@ -365,6 +442,7 @@ async function main() {
365
442
  console.error(`${PREFIX}`)
366
443
  console.error(`${PREFIX} No setup wizard needed — everything works out of the box.`)
367
444
  console.error(`${PREFIX} ChromaDB is optional (disabled by default, SQLite FTS5 is used).`)
445
+ console.error(`${PREFIX} To disable auto-start: claude-brain autostart uninstall`)
368
446
  console.error(`${PREFIX} ────────────────────────────────────────`)
369
447
  console.error('')
370
448
  }
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Auto-start integration for claude-brain server.
3
+ * Installs/uninstalls a shell profile snippet so the server
4
+ * auto-starts on every new terminal session.
5
+ *
6
+ * Pure Node.js — no Bun-specific APIs.
7
+ */
8
+
9
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'
10
+ import { join, dirname } from 'node:path'
11
+ import { homedir, platform } from 'node:os'
12
+
13
+ const START_MARKER = '# >>> claude-brain auto-start >>>'
14
+ const END_MARKER = '# <<< claude-brain auto-start <<<'
15
+
16
+ const PREFIX = '[claude-brain]'
17
+
18
+ function log(msg: string) {
19
+ console.error(`${PREFIX} ${msg}`)
20
+ }
21
+
22
+ export interface AutoStartResult {
23
+ success: boolean
24
+ message: string
25
+ profilePath: string
26
+ }
27
+
28
+ /**
29
+ * Build the Unix/macOS/Linux shell snippet.
30
+ * Uses `lsof` to check if the port is already in use before starting.
31
+ */
32
+ function buildUnixSnippet(port: number): string {
33
+ return [
34
+ START_MARKER,
35
+ '# Auto-start claude-brain server if not already running',
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 &',
38
+ ' disown 2>/dev/null',
39
+ '}',
40
+ END_MARKER,
41
+ ].join('\n')
42
+ }
43
+
44
+ /**
45
+ * Build the Fish shell snippet.
46
+ * Fish uses different syntax than bash/zsh.
47
+ */
48
+ function buildFishSnippet(port: number): string {
49
+ return [
50
+ START_MARKER,
51
+ '# Auto-start claude-brain server if not already running',
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 &',
54
+ 'end',
55
+ END_MARKER,
56
+ ].join('\n')
57
+ }
58
+
59
+ /**
60
+ * Build the PowerShell snippet for Windows.
61
+ */
62
+ function buildPowerShellSnippet(port: number): string {
63
+ return [
64
+ START_MARKER,
65
+ 'if (Get-Command claude-brain -ErrorAction SilentlyContinue) {',
66
+ ` $port = ${port}`,
67
+ ' $listening = Get-NetTCPConnection -LocalPort $port -ErrorAction SilentlyContinue',
68
+ ' if (-not $listening) {',
69
+ ' Start-Process -NoNewWindow -FilePath "claude-brain" -ArgumentList "serve" -WindowStyle Hidden',
70
+ ' }',
71
+ '}',
72
+ END_MARKER,
73
+ ].join('\n')
74
+ }
75
+
76
+ /**
77
+ * Detect available shell profile files based on platform.
78
+ * Returns paths that exist or are the primary target for the detected shell.
79
+ */
80
+ export function getShellProfiles(): string[] {
81
+ const home = homedir()
82
+ const os = platform()
83
+ const profiles: string[] = []
84
+
85
+ if (os === 'win32') {
86
+ // PowerShell profile
87
+ const psProfile = process.env.USERPROFILE
88
+ ? join(
89
+ process.env.USERPROFILE,
90
+ 'Documents',
91
+ 'PowerShell',
92
+ 'Microsoft.PowerShell_profile.ps1'
93
+ )
94
+ : null
95
+ // Also check WindowsPowerShell variant
96
+ const psProfileLegacy = process.env.USERPROFILE
97
+ ? join(
98
+ process.env.USERPROFILE,
99
+ 'Documents',
100
+ 'WindowsPowerShell',
101
+ 'Microsoft.PowerShell_profile.ps1'
102
+ )
103
+ : null
104
+
105
+ if (psProfile) profiles.push(psProfile)
106
+ if (psProfileLegacy && psProfileLegacy !== psProfile) profiles.push(psProfileLegacy)
107
+ return profiles
108
+ }
109
+
110
+ // Unix/macOS/Linux
111
+ if (os === 'darwin') {
112
+ // macOS defaults to zsh
113
+ profiles.push(join(home, '.zshrc'))
114
+ profiles.push(join(home, '.zprofile'))
115
+ } else {
116
+ // Linux defaults to bash
117
+ profiles.push(join(home, '.bashrc'))
118
+ profiles.push(join(home, '.bash_profile'))
119
+ }
120
+
121
+ // Fish (cross-platform Unix)
122
+ const fishConfig = join(home, '.config', 'fish', 'config.fish')
123
+ if (existsSync(fishConfig)) {
124
+ profiles.push(fishConfig)
125
+ }
126
+
127
+ return profiles
128
+ }
129
+
130
+ /**
131
+ * Check if the auto-start snippet is already in any profile file.
132
+ */
133
+ function profileContainsSnippet(profilePath: string): boolean {
134
+ if (!existsSync(profilePath)) return false
135
+ const content = readFileSync(profilePath, 'utf-8')
136
+ return content.includes(START_MARKER)
137
+ }
138
+
139
+ /**
140
+ * Get the appropriate snippet for a given profile path.
141
+ */
142
+ function getSnippetForProfile(profilePath: string, port: number): string {
143
+ if (profilePath.endsWith('.ps1')) {
144
+ return buildPowerShellSnippet(port)
145
+ }
146
+ if (profilePath.includes('fish')) {
147
+ return buildFishSnippet(port)
148
+ }
149
+ return buildUnixSnippet(port)
150
+ }
151
+
152
+ /**
153
+ * Install auto-start snippet into the primary shell profile.
154
+ * Idempotent — skips if already installed.
155
+ */
156
+ export function installAutoStart(port: number = 3000): AutoStartResult {
157
+ const profiles = getShellProfiles()
158
+
159
+ if (profiles.length === 0) {
160
+ return {
161
+ success: false,
162
+ message: 'No supported shell profile found',
163
+ profilePath: '',
164
+ }
165
+ }
166
+
167
+ // Use the first (primary) profile
168
+ const profilePath = profiles[0]
169
+
170
+ // Already installed?
171
+ if (profileContainsSnippet(profilePath)) {
172
+ log(`Auto-start already installed in ${profilePath}`)
173
+ return {
174
+ success: true,
175
+ message: 'Auto-start already installed',
176
+ profilePath,
177
+ }
178
+ }
179
+
180
+ // Ensure parent directory exists
181
+ const dir = dirname(profilePath)
182
+ if (!existsSync(dir)) {
183
+ mkdirSync(dir, { recursive: true })
184
+ }
185
+
186
+ // Read existing content or start fresh
187
+ const existing = existsSync(profilePath) ? readFileSync(profilePath, 'utf-8') : ''
188
+
189
+ // Append snippet with a blank line separator
190
+ const separator = existing.length > 0 && !existing.endsWith('\n') ? '\n\n' : '\n'
191
+ const snippet = getSnippetForProfile(profilePath, port)
192
+ writeFileSync(profilePath, existing + separator + snippet + '\n', 'utf-8')
193
+
194
+ log(`Auto-start installed in ${profilePath}`)
195
+ return {
196
+ success: true,
197
+ message: `Auto-start installed in ${profilePath}`,
198
+ profilePath,
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Remove auto-start snippet from all known shell profiles.
204
+ * Removes everything between and including the marker comments.
205
+ */
206
+ export function uninstallAutoStart(): AutoStartResult {
207
+ const profiles = getShellProfiles()
208
+ let removedFrom = ''
209
+
210
+ for (const profilePath of profiles) {
211
+ if (!existsSync(profilePath)) continue
212
+
213
+ const content = readFileSync(profilePath, 'utf-8')
214
+ if (!content.includes(START_MARKER)) continue
215
+
216
+ // Remove the snippet block (markers + everything between)
217
+ const startIdx = content.indexOf(START_MARKER)
218
+ const endIdx = content.indexOf(END_MARKER)
219
+
220
+ if (startIdx === -1 || endIdx === -1) continue
221
+
222
+ const endOfMarker = endIdx + END_MARKER.length
223
+ // Also remove trailing newline after end marker
224
+ const endWithNewline =
225
+ endOfMarker < content.length && content[endOfMarker] === '\n'
226
+ ? endOfMarker + 1
227
+ : endOfMarker
228
+
229
+ // Remove leading blank line if the snippet was appended with separator
230
+ let start = startIdx
231
+ if (start > 0 && content[start - 1] === '\n') {
232
+ start--
233
+ if (start > 0 && content[start - 1] === '\n') {
234
+ start--
235
+ }
236
+ }
237
+
238
+ const cleaned = content.slice(0, start) + content.slice(endWithNewline)
239
+ writeFileSync(profilePath, cleaned, 'utf-8')
240
+ removedFrom = profilePath
241
+ log(`Auto-start removed from ${profilePath}`)
242
+ }
243
+
244
+ if (!removedFrom) {
245
+ log('Auto-start was not installed in any profile')
246
+ return {
247
+ success: true,
248
+ message: 'Auto-start was not installed in any profile',
249
+ profilePath: '',
250
+ }
251
+ }
252
+
253
+ return {
254
+ success: true,
255
+ message: `Auto-start removed from ${removedFrom}`,
256
+ profilePath: removedFrom,
257
+ }
258
+ }
259
+
260
+ /**
261
+ * Check if auto-start is installed in any known shell profile.
262
+ */
263
+ export function isAutoStartInstalled(): boolean {
264
+ const profiles = getShellProfiles()
265
+ return profiles.some((p) => profileContainsSnippet(p))
266
+ }
package/src/cli/bin.ts CHANGED
@@ -34,6 +34,7 @@ function printHelp() {
34
34
  ['uninstall', 'Remove MCP server from Claude Code'],
35
35
  ['update', 'Update package and refresh CLAUDE.md'],
36
36
  ['refresh', 'Kill all, update to latest, and setup everything'],
37
+ ['autostart', 'Manage auto-start on terminal open (install/uninstall/status)'],
37
38
  ['chroma', 'Manage ChromaDB server (start/stop/status)'],
38
39
  ['hooks', 'Manage passive learning hooks (install/uninstall/status)'],
39
40
  ['git-hook', 'Manage git post-commit hook (install/uninstall/status)'],
@@ -142,6 +143,12 @@ async function main() {
142
143
  break
143
144
  }
144
145
 
146
+ case 'autostart': {
147
+ const { runAutoStart } = await import('./commands/autostart')
148
+ await runAutoStart()
149
+ break
150
+ }
151
+
145
152
  case 'chroma': {
146
153
  const { runChroma } = await import('./commands/chroma')
147
154
  await runChroma()
@@ -0,0 +1,90 @@
1
+ /**
2
+ * claude-brain autostart — Manage auto-start on terminal open.
3
+ *
4
+ * Usage:
5
+ * claude-brain autostart install Install auto-start snippet in shell profile
6
+ * claude-brain autostart uninstall Remove auto-start snippet from shell profiles
7
+ * claude-brain autostart status Check if auto-start is installed
8
+ */
9
+
10
+ import {
11
+ heading, successText, warningText, dimText, box,
12
+ } from '@/cli/ui/index.js'
13
+ import {
14
+ installAutoStart, uninstallAutoStart, isAutoStartInstalled, getShellProfiles,
15
+ } from '@/cli/auto-start'
16
+
17
+ export async function runAutoStart(): Promise<void> {
18
+ const subcommand = process.argv[3] || 'status'
19
+
20
+ switch (subcommand) {
21
+ case 'install': {
22
+ console.log()
23
+ console.log(heading('Auto-Start — Install'))
24
+ console.log()
25
+
26
+ const result = installAutoStart()
27
+ if (result.success) {
28
+ console.log(successText(` ${result.message}`))
29
+ if (result.profilePath) {
30
+ console.log(dimText(` Profile: ${result.profilePath}`))
31
+ console.log()
32
+ console.log(dimText(' Open a new terminal to activate.'))
33
+ }
34
+ } else {
35
+ console.log(warningText(` ${result.message}`))
36
+ }
37
+ console.log()
38
+ break
39
+ }
40
+
41
+ case 'uninstall': {
42
+ console.log()
43
+ console.log(heading('Auto-Start — Uninstall'))
44
+ console.log()
45
+
46
+ const result = uninstallAutoStart()
47
+ console.log(successText(` ${result.message}`))
48
+ console.log()
49
+ break
50
+ }
51
+
52
+ case 'status': {
53
+ console.log()
54
+ console.log(heading('Auto-Start — Status'))
55
+ console.log()
56
+
57
+ const installed = isAutoStartInstalled()
58
+ const profiles = getShellProfiles()
59
+
60
+ if (installed) {
61
+ console.log(successText(' Auto-start is installed'))
62
+ } else {
63
+ console.log(warningText(' Auto-start is not installed'))
64
+ }
65
+
66
+ console.log(dimText(` Shell profiles checked: ${profiles.join(', ') || 'none found'}`))
67
+ console.log()
68
+
69
+ if (!installed) {
70
+ console.log(box([
71
+ dimText('Install with:'),
72
+ ` claude-brain autostart install`,
73
+ ].join('\n'), 'Tip'))
74
+ console.log()
75
+ }
76
+ break
77
+ }
78
+
79
+ default: {
80
+ console.log()
81
+ console.log(warningText(`Unknown subcommand: ${subcommand}`))
82
+ console.log()
83
+ console.log(dimText('Usage:'))
84
+ console.log(dimText(' claude-brain autostart install Install auto-start'))
85
+ console.log(dimText(' claude-brain autostart uninstall Remove auto-start'))
86
+ console.log(dimText(' claude-brain autostart status Check status'))
87
+ console.log()
88
+ }
89
+ }
90
+ }
@@ -41,7 +41,7 @@ export async function runInstall() {
41
41
  } else {
42
42
  try {
43
43
  await withSpinner('Registering with Claude Code', async () => {
44
- execSync('claude mcp add claude-brain -- claude-brain serve', {
44
+ execSync('claude mcp add claude-brain -s user -- claude-brain serve', {
45
45
  encoding: 'utf-8',
46
46
  stdio: ['pipe', 'pipe', 'pipe']
47
47
  })
@@ -59,7 +59,7 @@ export async function runInstall() {
59
59
  errorText('Failed to register automatically.'),
60
60
  '',
61
61
  dimText('Run manually:'),
62
- ` ${theme.bold('claude mcp add claude-brain -- claude-brain serve')}`,
62
+ ` ${theme.bold('claude mcp add claude-brain -s user -- claude-brain serve')}`,
63
63
  ].join('\n'), 'Error'))
64
64
  }
65
65
  }
@@ -71,10 +71,10 @@ export async function runInstall() {
71
71
  console.log(box([
72
72
  `${theme.primary('Option 1:')} Install globally, then register`,
73
73
  ` ${theme.bold('bun install -g claude-brain')}`,
74
- ` ${theme.bold('claude mcp add claude-brain -- claude-brain serve')}`,
74
+ ` ${theme.bold('claude mcp add claude-brain -s user -- claude-brain serve')}`,
75
75
  '',
76
76
  `${theme.primary('Option 2:')} Use bunx ${dimText('(zero-install)')}`,
77
- ` ${theme.bold('claude mcp add claude-brain -- bunx claude-brain@latest')}`,
77
+ ` ${theme.bold('claude mcp add claude-brain -s user -- bunx claude-brain@latest')}`,
78
78
  '',
79
79
  `${theme.primary('Option 3:')} Add to Claude Code config manually`,
80
80
  dimText(' Add to your Claude Code MCP settings:'),
@@ -128,7 +128,7 @@ function isMcpConfigured(): boolean {
128
128
 
129
129
  function registerMcp(): boolean {
130
130
  try {
131
- execSync('claude mcp add claude-brain -- claude-brain serve', {
131
+ execSync('claude mcp add claude-brain -s user -- claude-brain serve', {
132
132
  encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 10_000,
133
133
  })
134
134
  return true
@@ -5,6 +5,7 @@ import { ClaudeBrainMCPServer } from '@/server'
5
5
  import { initializeServices, shutdownServices, getVaultService, getMemoryService } from '@/server/services'
6
6
  import { createOrchestrator, type Orchestrator } from '@/orchestrator'
7
7
  import { ensureHomeDirectory } from '@/cli/auto-setup'
8
+ import { ServerPidManager } from '@/server/pid-manager'
8
9
 
9
10
  const BANNER = `
10
11
  ╔═══════════════════════════════════════════════════════╗
@@ -17,6 +18,18 @@ export async function runServe() {
17
18
  // Auto-initialize home directory on first run
18
19
  ensureHomeDirectory()
19
20
 
21
+ // Singleton check: prevent multiple server instances
22
+ const pidManager = new ServerPidManager()
23
+ const existingPid = pidManager.getRunningPid()
24
+ if (existingPid) {
25
+ console.error(`[claude-brain] Server already running (PID: ${existingPid}). Exiting.`)
26
+ process.exit(0)
27
+ }
28
+
29
+ // Write PID file and register cleanup handlers
30
+ pidManager.writePidFile()
31
+ pidManager.registerCleanupHandlers()
32
+
20
33
  // Auto-install Claude Code hooks (idempotent, non-fatal)
21
34
  try {
22
35
  const { installHooks } = await import('@/hooks/installer')
@@ -118,6 +131,11 @@ export async function runServe() {
118
131
  await shutdownServices()
119
132
  })
120
133
 
134
+ // Clean up PID file during graceful shutdown
135
+ cleanup.register(async () => {
136
+ pidManager.cleanup()
137
+ })
138
+
121
139
  // Start HTTP API server alongside MCP server
122
140
  const { HttpApiServer } = await import('@/server/http-api')
123
141
  const httpServer = new HttpApiServer(config, logger)
@@ -190,6 +208,44 @@ export async function runServe() {
190
208
  }
191
209
  }
192
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
+
235
+ // Schedule periodic checks
236
+ autoUpdater.schedulePeriodicCheck()
237
+
238
+ cleanup.register(async () => {
239
+ autoUpdater?.stopPeriodicCheck()
240
+ mainLogger.info('Auto-updater stopped')
241
+ })
242
+
243
+ mainLogger.info('Auto-updater initialized')
244
+ } catch (error) {
245
+ mainLogger.warn({ error }, 'Failed to initialize auto-updater')
246
+ }
247
+ }
248
+
193
249
  // Start HTTP server after MCP server is ready
194
250
  setTimeout(async () => {
195
251
  try {
@@ -14,12 +14,12 @@ export async function runUninstall() {
14
14
  try {
15
15
  await withSpinner('Removing Claude Brain from Claude Code', async () => {
16
16
  try {
17
- execSync('claude mcp remove claude-brain', {
17
+ execSync('claude mcp remove claude-brain -s user', {
18
18
  encoding: 'utf-8',
19
19
  stdio: ['pipe', 'pipe', 'pipe'],
20
20
  })
21
21
  } catch {
22
- // Falls back to scoped removal when registered in multiple scopes
22
+ // Falls back to local scope removal in case it was registered there
23
23
  execSync('claude mcp remove claude-brain -s local', {
24
24
  encoding: 'utf-8',
25
25
  stdio: ['pipe', 'pipe', 'pipe'],
@@ -346,6 +346,16 @@ export const ConfigSchema = z.object({
346
346
  enabled: z.boolean().default(true),
347
347
  }).default({} as any),
348
348
 
349
+ /** Phase 31: Auto-update configuration */
350
+ autoUpdate: z.object({
351
+ /** Enable automatic update checks */
352
+ enabled: z.boolean().default(true),
353
+ /** Hours between update checks */
354
+ checkIntervalHours: z.number().int().min(1).max(168).default(24),
355
+ /** Automatically restart server after update */
356
+ autoRestart: z.boolean().default(true),
357
+ }).default({} as any),
358
+
349
359
  /** Phase 30: Optional LLM compression for observations */
350
360
  compression: z.object({
351
361
  /** Enable LLM-based compression of long observations */
@@ -211,7 +211,7 @@ export class PassiveClassifier {
211
211
  const packages = match[1]?.trim()
212
212
  if (packages) {
213
213
  return {
214
- type: 'decision',
214
+ type: 'progress',
215
215
  confidence: 0.85,
216
216
  content: `Installed package(s): ${packages}`,
217
217
  project: this.extractProjectFromCwd(input.cwd),