claude-brain 0.27.0 → 0.27.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/README.md +2 -2
- package/VERSION +1 -1
- package/package.json +1 -1
- package/src/cli/commands/install-mcp.ts +20 -1
- package/src/cli/commands/refresh.ts +19 -3
- package/src/hooks/brain-hook.ts +2 -2
- package/src/hooks/context-hook.ts +2 -2
- package/src/hooks/interceptor-hook.ts +2 -2
- package/src/server/handlers/call-tool.ts +7 -0
- package/src/server/handlers/tools/index.ts +3 -0
- package/src/server/handlers/tools/schemas.ts +15 -0
- package/src/server/handlers/tools/search-code.ts +122 -0
- package/src/tools/schemas.ts +44 -0
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ bun install -g claude-brain
|
|
|
32
32
|
claude-brain setup
|
|
33
33
|
|
|
34
34
|
# Register with Claude Code
|
|
35
|
-
claude mcp add claude-brain -- bunx claude-brain@latest
|
|
35
|
+
claude mcp add claude-brain -s user -- bunx claude-brain@latest
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
That's it. Every Claude Code session now has 25 brain tools available.
|
|
@@ -42,7 +42,7 @@ That's it. Every Claude Code session now has 25 brain tools available.
|
|
|
42
42
|
Skip the global install — just register with Claude Code directly:
|
|
43
43
|
|
|
44
44
|
```bash
|
|
45
|
-
claude mcp add claude-brain -- bunx claude-brain@latest
|
|
45
|
+
claude mcp add claude-brain -s user -- bunx claude-brain@latest
|
|
46
46
|
```
|
|
47
47
|
|
|
48
48
|
On first run, `~/.claude-brain/` is auto-created with default config.
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.27.
|
|
1
|
+
0.27.2
|
package/package.json
CHANGED
|
@@ -37,7 +37,26 @@ export async function runInstall() {
|
|
|
37
37
|
console.log()
|
|
38
38
|
|
|
39
39
|
if (isMcpAlreadyConfigured()) {
|
|
40
|
-
|
|
40
|
+
// Ensure user scope — remove local/project scope if present, then register at user scope
|
|
41
|
+
for (const scope of ['local', 'project']) {
|
|
42
|
+
try {
|
|
43
|
+
execSync(`claude mcp remove claude-brain -s ${scope}`, {
|
|
44
|
+
encoding: 'utf-8',
|
|
45
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
46
|
+
})
|
|
47
|
+
} catch {
|
|
48
|
+
// OK — may not exist at this scope
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
execSync('claude mcp add claude-brain -s user -- claude-brain serve', {
|
|
53
|
+
encoding: 'utf-8',
|
|
54
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
55
|
+
})
|
|
56
|
+
} catch {
|
|
57
|
+
// OK — may already be registered at user scope
|
|
58
|
+
}
|
|
59
|
+
console.log(box(successText('MCP registered at user scope. Claude Brain is available in all projects.'), 'Success'))
|
|
41
60
|
} else {
|
|
42
61
|
try {
|
|
43
62
|
await withSpinner('Registering with Claude Code', async () => {
|
|
@@ -115,6 +115,19 @@ function sleep(ms: number): Promise<void> {
|
|
|
115
115
|
return new Promise(resolve => setTimeout(resolve, ms))
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
function cleanupLocalMcpScopes(): void {
|
|
119
|
+
try {
|
|
120
|
+
execSync('claude mcp remove claude-brain -s local', {
|
|
121
|
+
encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 10_000,
|
|
122
|
+
})
|
|
123
|
+
} catch {}
|
|
124
|
+
try {
|
|
125
|
+
execSync('claude mcp remove claude-brain -s project', {
|
|
126
|
+
encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 10_000,
|
|
127
|
+
})
|
|
128
|
+
} catch {}
|
|
129
|
+
}
|
|
130
|
+
|
|
118
131
|
function isMcpConfigured(): boolean {
|
|
119
132
|
try {
|
|
120
133
|
const result = execSync('claude mcp list', {
|
|
@@ -279,9 +292,12 @@ export async function runRefresh() {
|
|
|
279
292
|
}
|
|
280
293
|
|
|
281
294
|
// 3e. MCP registration
|
|
282
|
-
const mcpOk = await withSpinner('
|
|
283
|
-
|
|
284
|
-
|
|
295
|
+
const mcpOk = await withSpinner('Ensuring MCP at user scope', async () => {
|
|
296
|
+
cleanupLocalMcpScopes()
|
|
297
|
+
if (!isMcpConfigured()) {
|
|
298
|
+
return registerMcp()
|
|
299
|
+
}
|
|
300
|
+
return true
|
|
285
301
|
})
|
|
286
302
|
if (mcpOk) {
|
|
287
303
|
results.push(['MCP', 'ok', 'Registered in Claude Code'])
|
package/src/hooks/brain-hook.ts
CHANGED
|
@@ -52,10 +52,10 @@ export async function main(): Promise<void> {
|
|
|
52
52
|
return
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
// BUG-006: Read port from --port arg (cross-platform)
|
|
55
|
+
// BUG-006: Read port from --port arg (cross-platform). Env var fallback removed — breaks Windows cmd.exe.
|
|
56
56
|
const portIdx = process.argv.indexOf('--port')
|
|
57
57
|
const portArg = portIdx >= 0 ? process.argv[portIdx + 1] : undefined
|
|
58
|
-
const port = parseInt(portArg ||
|
|
58
|
+
const port = parseInt(portArg || '3000', 10)
|
|
59
59
|
|
|
60
60
|
// For Stop events: trigger session-end summarization regardless of classification
|
|
61
61
|
if (input.hook_event_name === 'Stop' && input.session_id) {
|
|
@@ -85,10 +85,10 @@ async function main(): Promise<void> {
|
|
|
85
85
|
return
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
// BUG-006: Read port from --port arg (cross-platform)
|
|
88
|
+
// BUG-006: Read port from --port arg (cross-platform). Env var fallback removed — breaks Windows cmd.exe.
|
|
89
89
|
const portIdx = process.argv.indexOf('--port')
|
|
90
90
|
const portArg = portIdx >= 0 ? process.argv[portIdx + 1] : undefined
|
|
91
|
-
const port = parseInt(portArg ||
|
|
91
|
+
const port = parseInt(portArg || '3000', 10)
|
|
92
92
|
const baseUrl = `http://localhost:${port}`
|
|
93
93
|
|
|
94
94
|
let brainContext = ''
|
|
@@ -51,10 +51,10 @@ async function main(): Promise<void> {
|
|
|
51
51
|
const project = detectProject(input.cwd)
|
|
52
52
|
if (!project) { process.exit(0); return }
|
|
53
53
|
|
|
54
|
-
// Read port from --port arg
|
|
54
|
+
// BUG-006: Read port from --port arg (cross-platform). Env var fallback removed — breaks Windows cmd.exe.
|
|
55
55
|
const portIdx = process.argv.indexOf('--port')
|
|
56
56
|
const portArg = portIdx >= 0 ? process.argv[portIdx + 1] : undefined
|
|
57
|
-
const port = parseInt(portArg ||
|
|
57
|
+
const port = parseInt(portArg || '3000', 10)
|
|
58
58
|
|
|
59
59
|
try {
|
|
60
60
|
const params = new URLSearchParams({
|
|
@@ -34,6 +34,9 @@ import { handleInitProject } from './tools/init-project'
|
|
|
34
34
|
// Phase 16 Unified Brain Tool
|
|
35
35
|
import { handleBrain } from './tools/brain'
|
|
36
36
|
|
|
37
|
+
// Code Intelligence
|
|
38
|
+
import { handleSearchCode } from './tools/search-code'
|
|
39
|
+
|
|
37
40
|
/**
|
|
38
41
|
* Handle tools/call request
|
|
39
42
|
* Validates that tool exists and routes to the appropriate handler
|
|
@@ -122,6 +125,10 @@ export async function handleCallTool(
|
|
|
122
125
|
case 'brain':
|
|
123
126
|
return await handleBrain(args, logger)
|
|
124
127
|
|
|
128
|
+
// Code Intelligence
|
|
129
|
+
case 'search_code':
|
|
130
|
+
return await handleSearchCode(args, logger)
|
|
131
|
+
|
|
125
132
|
default:
|
|
126
133
|
// This should never happen if validateToolExists works correctly
|
|
127
134
|
throw new McpError(
|
|
@@ -25,3 +25,6 @@ export { handleCreateProject } from './create-project'
|
|
|
25
25
|
export { handleInitProject } from './init-project'
|
|
26
26
|
|
|
27
27
|
// Phase 19: Redundant tools absorbed into brain(). Handler files removed.
|
|
28
|
+
|
|
29
|
+
// Code Intelligence
|
|
30
|
+
export { handleSearchCode } from './search-code'
|
|
@@ -141,6 +141,18 @@ export const BrainSchema = z.object({
|
|
|
141
141
|
action: z.enum(['auto', 'store', 'recall', 'update', 'delete']).optional()
|
|
142
142
|
})
|
|
143
143
|
|
|
144
|
+
// ============================================================================
|
|
145
|
+
// Code Intelligence
|
|
146
|
+
// ============================================================================
|
|
147
|
+
|
|
148
|
+
export const SearchCodeSchema = z.object({
|
|
149
|
+
query: z.string().min(1, 'query is required'),
|
|
150
|
+
project: z.string().min(1, 'project is required'),
|
|
151
|
+
type: z.enum(['symbols', 'files', 'dependencies']).optional().default('symbols'),
|
|
152
|
+
file_path: z.string().optional(),
|
|
153
|
+
limit: z.number().int().min(1).max(100).optional().default(20)
|
|
154
|
+
})
|
|
155
|
+
|
|
144
156
|
// ============================================================================
|
|
145
157
|
// Type exports for validated inputs
|
|
146
158
|
// ============================================================================
|
|
@@ -162,3 +174,6 @@ export type ValidatedGetCorrections = z.infer<typeof GetCorrectionsSchema>
|
|
|
162
174
|
|
|
163
175
|
// Phase 16
|
|
164
176
|
export type ValidatedBrain = z.infer<typeof BrainSchema>
|
|
177
|
+
|
|
178
|
+
// Code Intelligence
|
|
179
|
+
export type ValidatedSearchCode = z.infer<typeof SearchCodeSchema>
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search Code Handler
|
|
3
|
+
* Exposes code intelligence (symbol/file/dependency search) as a direct MCP tool
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Logger } from 'pino'
|
|
7
|
+
import type { ToolResponse } from '@/tools/types'
|
|
8
|
+
import { getCodeQuery, isServicesInitialized } from '@/server/services'
|
|
9
|
+
import { ToolValidator } from '@/server/utils/validators'
|
|
10
|
+
import { ResponseFormatter } from '@/server/utils/response-formatter'
|
|
11
|
+
import { ErrorHandler } from '@/server/utils/error-handler'
|
|
12
|
+
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js'
|
|
13
|
+
import { SearchCodeSchema } from './schemas'
|
|
14
|
+
|
|
15
|
+
export async function handleSearchCode(
|
|
16
|
+
args: unknown,
|
|
17
|
+
logger: Logger
|
|
18
|
+
): Promise<ToolResponse> {
|
|
19
|
+
try {
|
|
20
|
+
const input = ToolValidator.validate(args, SearchCodeSchema)
|
|
21
|
+
const { query, project, type, file_path, limit } = input
|
|
22
|
+
|
|
23
|
+
logger.debug({ query, project, type, limit }, 'Searching code index')
|
|
24
|
+
|
|
25
|
+
if (!isServicesInitialized()) {
|
|
26
|
+
throw new McpError(
|
|
27
|
+
ErrorCode.InternalError,
|
|
28
|
+
'Services not initialized. The server may still be starting up.'
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const codeQuery = getCodeQuery()
|
|
33
|
+
if (!codeQuery) {
|
|
34
|
+
return ResponseFormatter.text(
|
|
35
|
+
'Code intelligence is not available. Run `claude-brain reindex` to index your project first.'
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (type === 'files') {
|
|
40
|
+
const results = codeQuery.findFiles(query, project)
|
|
41
|
+
if (results.length === 0) {
|
|
42
|
+
return ResponseFormatter.text(
|
|
43
|
+
`No files found matching "${query}" in project "${project}". Try \`claude-brain reindex\` if the project hasn't been indexed.`
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const lines = results.map(r => {
|
|
48
|
+
const parts = [r.filePath]
|
|
49
|
+
if (r.language) parts.push(`(${r.language})`)
|
|
50
|
+
if (r.summary) parts.push(`— ${r.summary}`)
|
|
51
|
+
parts.push(`[${r.symbolCount} symbols, ${r.lineCount} lines]`)
|
|
52
|
+
return parts.join(' ')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
return ResponseFormatter.text(
|
|
56
|
+
`Found ${results.length} file${results.length === 1 ? '' : 's'} matching "${query}":\n\n${lines.join('\n')}`
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (type === 'dependencies') {
|
|
61
|
+
const targetPath = file_path || query
|
|
62
|
+
const deps = codeQuery.getDependencies(targetPath, project)
|
|
63
|
+
|
|
64
|
+
const parts: string[] = [`Dependencies for ${deps.file}:\n`]
|
|
65
|
+
|
|
66
|
+
if (deps.imports.length > 0) {
|
|
67
|
+
parts.push('**Imports:**')
|
|
68
|
+
for (const imp of deps.imports) {
|
|
69
|
+
const names = imp.names.length > 0 ? ` { ${imp.names.join(', ')} }` : ''
|
|
70
|
+
parts.push(` → ${imp.file}${names}`)
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
parts.push('**Imports:** none')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
parts.push('')
|
|
77
|
+
|
|
78
|
+
if (deps.importedBy.length > 0) {
|
|
79
|
+
parts.push('**Imported by:**')
|
|
80
|
+
for (const dep of deps.importedBy) {
|
|
81
|
+
const names = dep.names.length > 0 ? ` { ${dep.names.join(', ')} }` : ''
|
|
82
|
+
parts.push(` ← ${dep.file}${names}`)
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
parts.push('**Imported by:** none')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return ResponseFormatter.text(parts.join('\n'))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Default: symbols
|
|
92
|
+
const results = codeQuery.findSymbols(query, project, limit)
|
|
93
|
+
if (results.length === 0) {
|
|
94
|
+
return ResponseFormatter.text(
|
|
95
|
+
`No symbols found matching "${query}" in project "${project}". Try \`claude-brain reindex\` if the project hasn't been indexed.`
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const lines = results.map(r => {
|
|
100
|
+
const sig = r.signature ? ` ${r.signature}` : ''
|
|
101
|
+
const lineRange = r.lineEnd ? `${r.lineStart}-${r.lineEnd}` : `${r.lineStart}`
|
|
102
|
+
const conf = r.confidence < 1.0 ? ` (${(r.confidence * 100).toFixed(0)}%)` : ''
|
|
103
|
+
return `${r.symbol} (${r.type})${sig} → ${r.filePath}:${lineRange}${conf}`
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
return ResponseFormatter.text(
|
|
107
|
+
`Found ${results.length} symbol${results.length === 1 ? '' : 's'} matching "${query}":\n\n${lines.join('\n')}`
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
} catch (error) {
|
|
111
|
+
ErrorHandler.logError(logger, error, { tool: 'search_code', args })
|
|
112
|
+
|
|
113
|
+
if (error instanceof McpError) {
|
|
114
|
+
throw error
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
throw new McpError(
|
|
118
|
+
ErrorCode.InternalError,
|
|
119
|
+
`Failed to search code: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
}
|
package/src/tools/schemas.ts
CHANGED
|
@@ -610,6 +610,50 @@ export const TOOLS = {
|
|
|
610
610
|
},
|
|
611
611
|
required: ['message']
|
|
612
612
|
}
|
|
613
|
+
},
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* SEARCH_CODE
|
|
617
|
+
* Search indexed code symbols, files, and dependencies
|
|
618
|
+
*
|
|
619
|
+
* Use this when:
|
|
620
|
+
* - Looking for a function, class, or type definition
|
|
621
|
+
* - Need to find which file contains a symbol
|
|
622
|
+
* - Want to see imports/dependencies of a file
|
|
623
|
+
* - Faster than grep for known symbol names
|
|
624
|
+
*/
|
|
625
|
+
SEARCH_CODE: {
|
|
626
|
+
name: 'search_code',
|
|
627
|
+
description: 'Search indexed code for symbols (functions, classes, types), files, or dependencies. Faster than grep when the project is indexed. Run `claude-brain reindex` first if no results.',
|
|
628
|
+
inputSchema: {
|
|
629
|
+
type: 'object' as const,
|
|
630
|
+
properties: {
|
|
631
|
+
query: {
|
|
632
|
+
type: 'string',
|
|
633
|
+
description: 'Symbol name, file name, or search term'
|
|
634
|
+
},
|
|
635
|
+
project: {
|
|
636
|
+
type: 'string',
|
|
637
|
+
description: 'Project name (usually the directory name, e.g. "claude-brain")'
|
|
638
|
+
},
|
|
639
|
+
type: {
|
|
640
|
+
type: 'string',
|
|
641
|
+
enum: ['symbols', 'files', 'dependencies'],
|
|
642
|
+
description: 'What to search for: symbols (functions/classes/types), files, or dependencies of a file',
|
|
643
|
+
default: 'symbols'
|
|
644
|
+
},
|
|
645
|
+
file_path: {
|
|
646
|
+
type: 'string',
|
|
647
|
+
description: 'File path (required when type is "dependencies")'
|
|
648
|
+
},
|
|
649
|
+
limit: {
|
|
650
|
+
type: 'number',
|
|
651
|
+
description: 'Maximum results to return',
|
|
652
|
+
default: 20
|
|
653
|
+
}
|
|
654
|
+
},
|
|
655
|
+
required: ['query', 'project']
|
|
656
|
+
}
|
|
613
657
|
}
|
|
614
658
|
} as const satisfies Record<string, ToolDefinition>
|
|
615
659
|
|