claude-brain 0.23.0 → 0.24.1
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 +69 -0
- package/src/cli/commands/uninstall-mcp.ts +2 -2
- package/src/config/schema.ts +10 -0
- package/src/server/auto-updater.ts +301 -0
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.24.1
|
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
|
|
@@ -41,6 +41,37 @@ export async function runServe() {
|
|
|
41
41
|
console.error(`[claude-brain] Hook auto-install skipped: ${error instanceof Error ? error.message : 'unknown error'}`)
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
// Auto-install shell auto-start if not already present (handles bun blocking postinstall)
|
|
45
|
+
try {
|
|
46
|
+
const { isAutoStartInstalled, installAutoStart } = await import('@/cli/auto-start')
|
|
47
|
+
if (!isAutoStartInstalled()) {
|
|
48
|
+
const result = installAutoStart()
|
|
49
|
+
if (result.success && result.profilePath) {
|
|
50
|
+
console.error(`[claude-brain] Auto-start installed in ${result.profilePath}`)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
} catch (error) {
|
|
54
|
+
// Non-fatal — auto-start is a convenience feature
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Auto-install CLAUDE.md if not present (handles bun blocking postinstall)
|
|
58
|
+
try {
|
|
59
|
+
const { existsSync, readFileSync, writeFileSync, mkdirSync } = await import('node:fs')
|
|
60
|
+
const { join, resolve, dirname } = await import('node:path')
|
|
61
|
+
const { homedir } = await import('node:os')
|
|
62
|
+
const { fileURLToPath } = await import('node:url')
|
|
63
|
+
const claudeMdDest = join(homedir(), '.claude', 'CLAUDE.md')
|
|
64
|
+
const pkgRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', '..')
|
|
65
|
+
const claudeMdSrc = join(pkgRoot, 'assets', 'CLAUDE.md')
|
|
66
|
+
if (existsSync(claudeMdSrc) && !existsSync(claudeMdDest)) {
|
|
67
|
+
mkdirSync(join(homedir(), '.claude'), { recursive: true })
|
|
68
|
+
writeFileSync(claudeMdDest, readFileSync(claudeMdSrc, 'utf-8'), 'utf-8')
|
|
69
|
+
console.error('[claude-brain] CLAUDE.md installed to ~/.claude/CLAUDE.md')
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
// Non-fatal
|
|
73
|
+
}
|
|
74
|
+
|
|
44
75
|
const config = await loadConfig()
|
|
45
76
|
|
|
46
77
|
if (config.logLevel === 'debug' || config.logLevel === 'info') {
|
|
@@ -208,6 +239,44 @@ export async function runServe() {
|
|
|
208
239
|
}
|
|
209
240
|
}
|
|
210
241
|
|
|
242
|
+
// Phase 31: Auto-update checker
|
|
243
|
+
let autoUpdater: InstanceType<typeof import('@/server/auto-updater').AutoUpdater> | null = null
|
|
244
|
+
if (config.autoUpdate?.enabled !== false) {
|
|
245
|
+
try {
|
|
246
|
+
const { AutoUpdater } = await import('@/server/auto-updater')
|
|
247
|
+
autoUpdater = new AutoUpdater(
|
|
248
|
+
{
|
|
249
|
+
enabled: config.autoUpdate?.enabled ?? true,
|
|
250
|
+
checkIntervalHours: config.autoUpdate?.checkIntervalHours ?? 24,
|
|
251
|
+
autoRestart: config.autoUpdate?.autoRestart ?? true,
|
|
252
|
+
},
|
|
253
|
+
mainLogger
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
// Check for updates on startup (non-blocking)
|
|
257
|
+
autoUpdater.check().then(result => {
|
|
258
|
+
if (result.updateAvailable && result.latestVersion) {
|
|
259
|
+
mainLogger.info(
|
|
260
|
+
{ current: result.currentVersion, latest: result.latestVersion },
|
|
261
|
+
'Update available! Run: claude-brain update'
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
}).catch(() => {})
|
|
265
|
+
|
|
266
|
+
// Schedule periodic checks
|
|
267
|
+
autoUpdater.schedulePeriodicCheck()
|
|
268
|
+
|
|
269
|
+
cleanup.register(async () => {
|
|
270
|
+
autoUpdater?.stopPeriodicCheck()
|
|
271
|
+
mainLogger.info('Auto-updater stopped')
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
mainLogger.info('Auto-updater initialized')
|
|
275
|
+
} catch (error) {
|
|
276
|
+
mainLogger.warn({ error }, 'Failed to initialize auto-updater')
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
211
280
|
// Start HTTP server after MCP server is ready
|
|
212
281
|
setTimeout(async () => {
|
|
213
282
|
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 */
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-updater for claude-brain.
|
|
3
|
+
* Checks npm for new versions periodically, auto-updates with ghost process cleanup and fresh restart.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { execSync, spawn } from 'node:child_process'
|
|
7
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from 'node:fs'
|
|
8
|
+
import { join, dirname, resolve } from 'node:path'
|
|
9
|
+
import { homedir, platform } from 'node:os'
|
|
10
|
+
import { fileURLToPath } from 'node:url'
|
|
11
|
+
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
13
|
+
const __dirname = dirname(__filename)
|
|
14
|
+
const PACKAGE_ROOT = resolve(__dirname, '..', '..')
|
|
15
|
+
|
|
16
|
+
interface UpdateCheckResult {
|
|
17
|
+
currentVersion: string
|
|
18
|
+
latestVersion: string | null
|
|
19
|
+
updateAvailable: boolean
|
|
20
|
+
lastChecked: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface AutoUpdateConfig {
|
|
24
|
+
enabled: boolean
|
|
25
|
+
checkIntervalHours: number
|
|
26
|
+
autoRestart: boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const DEFAULT_CONFIG: AutoUpdateConfig = {
|
|
30
|
+
enabled: true,
|
|
31
|
+
checkIntervalHours: 24,
|
|
32
|
+
autoRestart: true,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const isWindows = platform() === 'win32'
|
|
36
|
+
|
|
37
|
+
export class AutoUpdater {
|
|
38
|
+
private dataDir: string
|
|
39
|
+
private checkFile: string
|
|
40
|
+
private config: AutoUpdateConfig
|
|
41
|
+
private timer: ReturnType<typeof setInterval> | null = null
|
|
42
|
+
private logger: { info: (...args: any[]) => void; warn: (...args: any[]) => void; error: (...args: any[]) => void }
|
|
43
|
+
|
|
44
|
+
constructor(config?: Partial<AutoUpdateConfig>, logger?: any) {
|
|
45
|
+
this.config = { ...DEFAULT_CONFIG, ...config }
|
|
46
|
+
this.dataDir = join(homedir(), '.claude-brain', 'data')
|
|
47
|
+
this.checkFile = join(this.dataDir, 'update-check.json')
|
|
48
|
+
this.logger = logger ?? {
|
|
49
|
+
info: (...args: any[]) => console.error('[auto-updater] INFO:', ...args),
|
|
50
|
+
warn: (...args: any[]) => console.error('[auto-updater] WARN:', ...args),
|
|
51
|
+
error: (...args: any[]) => console.error('[auto-updater] ERROR:', ...args),
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Check npm registry for latest version */
|
|
56
|
+
async check(): Promise<UpdateCheckResult> {
|
|
57
|
+
const currentVersion = this.getCurrentVersion()
|
|
58
|
+
|
|
59
|
+
// Return cached result if checked recently
|
|
60
|
+
if (!this.shouldCheck()) {
|
|
61
|
+
try {
|
|
62
|
+
const cached = JSON.parse(readFileSync(this.checkFile, 'utf-8')) as UpdateCheckResult
|
|
63
|
+
return { ...cached, currentVersion }
|
|
64
|
+
} catch {
|
|
65
|
+
// Corrupted cache, proceed with fresh check
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let latestVersion: string | null = null
|
|
70
|
+
try {
|
|
71
|
+
const result = execSync('npm view claude-brain version', {
|
|
72
|
+
encoding: 'utf-8',
|
|
73
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
74
|
+
timeout: 15_000,
|
|
75
|
+
})
|
|
76
|
+
latestVersion = result.trim()
|
|
77
|
+
} catch {
|
|
78
|
+
this.logger.warn('Failed to check npm registry for latest version')
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const updateAvailable = latestVersion !== null && latestVersion !== currentVersion
|
|
82
|
+
const checkResult: UpdateCheckResult = {
|
|
83
|
+
currentVersion,
|
|
84
|
+
latestVersion,
|
|
85
|
+
updateAvailable,
|
|
86
|
+
lastChecked: new Date().toISOString(),
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Persist check result
|
|
90
|
+
try {
|
|
91
|
+
mkdirSync(this.dataDir, { recursive: true })
|
|
92
|
+
writeFileSync(this.checkFile, JSON.stringify(checkResult, null, 2), 'utf-8')
|
|
93
|
+
} catch (err) {
|
|
94
|
+
this.logger.warn('Failed to write update check file:', err)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (updateAvailable) {
|
|
98
|
+
this.logger.info(`Update available: v${currentVersion} -> v${latestVersion}`)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return checkResult
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Perform the update: kill ghosts -> update package -> restart */
|
|
105
|
+
async performUpdate(targetVersion: string): Promise<boolean> {
|
|
106
|
+
try {
|
|
107
|
+
this.logger.info(`Updating claude-brain to v${targetVersion}...`)
|
|
108
|
+
|
|
109
|
+
// Kill ghost processes
|
|
110
|
+
this.killGhostProcesses()
|
|
111
|
+
|
|
112
|
+
// Wait for ports to release
|
|
113
|
+
await this.sleep(2000)
|
|
114
|
+
|
|
115
|
+
// Try bun first, fallback to npm
|
|
116
|
+
try {
|
|
117
|
+
execSync('bun install -g claude-brain@latest', {
|
|
118
|
+
encoding: 'utf-8',
|
|
119
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
120
|
+
timeout: 120_000,
|
|
121
|
+
})
|
|
122
|
+
this.logger.info(`Updated to v${targetVersion} via bun`)
|
|
123
|
+
} catch {
|
|
124
|
+
try {
|
|
125
|
+
execSync('npm install -g claude-brain@latest', {
|
|
126
|
+
encoding: 'utf-8',
|
|
127
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
128
|
+
timeout: 120_000,
|
|
129
|
+
})
|
|
130
|
+
this.logger.info(`Updated to v${targetVersion} via npm`)
|
|
131
|
+
} catch (err) {
|
|
132
|
+
this.logger.error('Install failed with both bun and npm:', err)
|
|
133
|
+
return false
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (this.config.autoRestart) {
|
|
138
|
+
this.restartServer()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return true
|
|
142
|
+
} catch (err) {
|
|
143
|
+
this.logger.error('Update failed:', err)
|
|
144
|
+
return false
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Schedule periodic background checks */
|
|
149
|
+
schedulePeriodicCheck(): void {
|
|
150
|
+
if (!this.config.enabled) return
|
|
151
|
+
if (this.timer) return
|
|
152
|
+
|
|
153
|
+
const intervalMs = this.config.checkIntervalHours * 3600 * 1000
|
|
154
|
+
|
|
155
|
+
this.timer = setInterval(async () => {
|
|
156
|
+
try {
|
|
157
|
+
const result = await this.check()
|
|
158
|
+
if (result.updateAvailable && result.latestVersion && this.config.enabled) {
|
|
159
|
+
await this.performUpdate(result.latestVersion)
|
|
160
|
+
}
|
|
161
|
+
} catch (err) {
|
|
162
|
+
this.logger.error('Periodic update check failed:', err)
|
|
163
|
+
}
|
|
164
|
+
}, intervalMs)
|
|
165
|
+
|
|
166
|
+
// Don't keep the process alive just for the update timer
|
|
167
|
+
if (this.timer && typeof this.timer === 'object' && 'unref' in this.timer) {
|
|
168
|
+
this.timer.unref()
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
this.logger.info(`Scheduled update checks every ${this.config.checkIntervalHours}h`)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Stop periodic checks */
|
|
175
|
+
stopPeriodicCheck(): void {
|
|
176
|
+
if (this.timer) {
|
|
177
|
+
clearInterval(this.timer)
|
|
178
|
+
this.timer = null
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Check if enough time has passed since last check */
|
|
183
|
+
private shouldCheck(): boolean {
|
|
184
|
+
if (!existsSync(this.checkFile)) return true
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
const data = JSON.parse(readFileSync(this.checkFile, 'utf-8')) as UpdateCheckResult
|
|
188
|
+
if (!data.lastChecked) return true
|
|
189
|
+
|
|
190
|
+
const lastChecked = new Date(data.lastChecked).getTime()
|
|
191
|
+
const intervalMs = this.config.checkIntervalHours * 3600 * 1000
|
|
192
|
+
return Date.now() - lastChecked >= intervalMs
|
|
193
|
+
} catch {
|
|
194
|
+
return true
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Kill all ghost claude-brain processes */
|
|
199
|
+
private killGhostProcesses(): void {
|
|
200
|
+
const myPid = process.pid
|
|
201
|
+
|
|
202
|
+
// Kill by pattern
|
|
203
|
+
try {
|
|
204
|
+
if (isWindows) {
|
|
205
|
+
const result = execSync(
|
|
206
|
+
`wmic process where "commandline like '%claude-brain%' and processid != ${myPid}" get processid /format:list`,
|
|
207
|
+
{ encoding: 'utf-8', stdio: 'pipe', timeout: 5000 }
|
|
208
|
+
)
|
|
209
|
+
const pids = result.match(/ProcessId=(\d+)/g)?.map(m => m.split('=')[1]) || []
|
|
210
|
+
for (const pid of pids) {
|
|
211
|
+
try { execSync(`taskkill /F /PID ${pid}`, { stdio: 'pipe', timeout: 5000 }) } catch {}
|
|
212
|
+
}
|
|
213
|
+
} else {
|
|
214
|
+
const result = execSync(`pgrep -f "claude-brain"`, {
|
|
215
|
+
encoding: 'utf-8', stdio: 'pipe', timeout: 5000,
|
|
216
|
+
})
|
|
217
|
+
const pids = result.trim().split('\n').filter(p => p && Number(p) !== myPid)
|
|
218
|
+
for (const pid of pids) {
|
|
219
|
+
try { process.kill(Number(pid), 'SIGKILL') } catch {}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
} catch {
|
|
223
|
+
// No matching processes — that's fine
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Kill by port 3000
|
|
227
|
+
try {
|
|
228
|
+
if (isWindows) {
|
|
229
|
+
const result = execSync(`netstat -ano | findstr :3000 | findstr LISTENING`, {
|
|
230
|
+
encoding: 'utf-8', stdio: 'pipe', timeout: 5000,
|
|
231
|
+
})
|
|
232
|
+
const pids = new Set(
|
|
233
|
+
result.split('\n')
|
|
234
|
+
.map(line => line.trim().split(/\s+/).pop())
|
|
235
|
+
.filter(p => p && Number(p) !== myPid)
|
|
236
|
+
)
|
|
237
|
+
for (const pid of pids) {
|
|
238
|
+
try { execSync(`taskkill /F /PID ${pid}`, { stdio: 'pipe', timeout: 5000 }) } catch {}
|
|
239
|
+
}
|
|
240
|
+
} else {
|
|
241
|
+
const raw = execSync(`lsof -ti :3000`, {
|
|
242
|
+
encoding: 'utf-8', stdio: 'pipe', timeout: 5000,
|
|
243
|
+
}).trim()
|
|
244
|
+
if (raw) {
|
|
245
|
+
const pids = raw.split('\n').filter(p => p && Number(p) !== myPid)
|
|
246
|
+
for (const pid of pids) {
|
|
247
|
+
try { process.kill(Number(pid), 'SIGKILL') } catch {}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
} catch {
|
|
252
|
+
// No process on port — that's fine
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Clean up stale PID files
|
|
256
|
+
const pidPath = join(this.dataDir, 'server.pid')
|
|
257
|
+
if (existsSync(pidPath)) {
|
|
258
|
+
try {
|
|
259
|
+
const pid = parseInt(readFileSync(pidPath, 'utf-8').trim(), 10)
|
|
260
|
+
if (!isNaN(pid) && pid !== myPid) {
|
|
261
|
+
try { process.kill(pid, 0) } catch {
|
|
262
|
+
// Process not running, remove stale PID file
|
|
263
|
+
try { unlinkSync(pidPath) } catch {}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
} catch {}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Spawn a fresh server instance and exit current */
|
|
271
|
+
private restartServer(): void {
|
|
272
|
+
this.logger.info('Restarting server with updated version...')
|
|
273
|
+
|
|
274
|
+
const child = spawn('claude-brain', ['serve'], {
|
|
275
|
+
detached: true,
|
|
276
|
+
stdio: 'ignore',
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
child.unref()
|
|
280
|
+
|
|
281
|
+
this.logger.info(`Spawned new server process (PID: ${child.pid}). Exiting current process.`)
|
|
282
|
+
process.exit(0)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Get current installed version from package.json */
|
|
286
|
+
private getCurrentVersion(): string {
|
|
287
|
+
try {
|
|
288
|
+
// Use the version from the package that's actually running
|
|
289
|
+
const pkgPath = join(PACKAGE_ROOT, 'package.json')
|
|
290
|
+
if (existsSync(pkgPath)) {
|
|
291
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
|
|
292
|
+
return pkg.version || 'unknown'
|
|
293
|
+
}
|
|
294
|
+
} catch {}
|
|
295
|
+
return 'unknown'
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private sleep(ms: number): Promise<void> {
|
|
299
|
+
return new Promise(resolve => setTimeout(resolve, ms))
|
|
300
|
+
}
|
|
301
|
+
}
|