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 +1 -1
- package/package.json +1 -1
- package/scripts/postinstall.mjs +81 -3
- package/src/cli/auto-start.ts +266 -0
- package/src/cli/bin.ts +7 -0
- package/src/cli/commands/autostart.ts +90 -0
- package/src/cli/commands/install-mcp.ts +4 -4
- package/src/cli/commands/refresh.ts +1 -1
- package/src/cli/commands/serve.ts +56 -0
- package/src/cli/commands/uninstall-mcp.ts +2 -2
- package/src/config/schema.ts +10 -0
- package/src/hooks/passive-classifier.ts +1 -1
- package/src/memory/fts5-search.ts +149 -0
- package/src/memory/index.ts +162 -41
- package/src/memory/migrations/add-fts5.ts +10 -0
- package/src/routing/intent-classifier.ts +7 -1
- package/src/server/auto-updater.ts +301 -0
- package/src/server/http-api.ts +11 -2
- package/src/server/pid-manager.ts +64 -0
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.24.0
|
package/package.json
CHANGED
package/scripts/postinstall.mjs
CHANGED
|
@@ -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}
|
|
359
|
-
console.error(`${PREFIX}
|
|
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
|
|
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'],
|
package/src/config/schema.ts
CHANGED
|
@@ -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: '
|
|
214
|
+
type: 'progress',
|
|
215
215
|
confidence: 0.85,
|
|
216
216
|
content: `Installed package(s): ${packages}`,
|
|
217
217
|
project: this.extractProjectFromCwd(input.cwd),
|