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 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.0
1
+ 0.27.2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-brain",
3
- "version": "0.27.0",
3
+ "version": "0.27.2",
4
4
  "description": "Local development assistant bridging Obsidian vaults with Claude Code via MCP",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -37,7 +37,26 @@ export async function runInstall() {
37
37
  console.log()
38
38
 
39
39
  if (isMcpAlreadyConfigured()) {
40
- console.log(box(successText('Already MCP configured. Claude Brain is registered as an MCP server.'), 'Success'))
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('Checking MCP registration', async () => {
283
- if (isMcpConfigured()) return true
284
- return registerMcp()
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'])
@@ -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) or env var fallback
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 || process.env.CLAUDE_BRAIN_PORT || '3000', 10)
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) or env var fallback
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 || process.env.CLAUDE_BRAIN_PORT || '3000', 10)
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 or env
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 || process.env.CLAUDE_BRAIN_PORT || '3000', 10)
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
+ }
@@ -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