claude-brain 0.26.0 → 0.27.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/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.26.0
1
+ 0.27.1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-brain",
3
- "version": "0.26.0",
3
+ "version": "0.27.1",
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'])
@@ -13,7 +13,7 @@
13
13
  import type { Logger } from 'pino'
14
14
  import { IntentClassifier, type ClassificationResult } from './intent-classifier'
15
15
  import { BrainEntityExtractor, type BrainExtractedEntities } from './entity-extractor'
16
- import { ResponseFilter, type BrainResponse, type TierResults, formatCompactResponse, formatDetailResponse, formatTimeline, groupByDay } from './response-filter'
16
+ import { ResponseFilter, type BrainResponse, type TierResults, type FilterableResult, formatCompactResponse, formatDetailResponse, formatTimeline, groupByDay } from './response-filter'
17
17
  import { SearchEngine } from './search-engine'
18
18
  import type { NormalizedResult } from './types'
19
19
  import {
@@ -24,6 +24,7 @@ import {
24
24
  getEpisodeService,
25
25
  getSessionTracker,
26
26
  getCodeLinker,
27
+ getCodeQuery,
27
28
  isServicesInitialized
28
29
  } from '@/server/services'
29
30
  import { timed } from '@/utils/timing'
@@ -275,6 +276,21 @@ export class BrainRouter {
275
276
  }
276
277
  }
277
278
 
279
+ // Phase 32: Include code file map in session context if code index is available
280
+ try {
281
+ const codeQuery = getCodeQuery()
282
+ if (codeQuery && project) {
283
+ const fileMap = codeQuery.getFileMap(project, 50)
284
+ if (fileMap.length > 0) {
285
+ const formatted = codeQuery.formatFileMap(fileMap)
286
+ parts.push('\n---\n## Code Structure\n')
287
+ parts.push(formatted)
288
+ }
289
+ }
290
+ } catch {
291
+ // Code intelligence not available
292
+ }
293
+
278
294
  // C8: Register with episode manager
279
295
  this.registerEpisodeMessage(message, project, 'session_start')
280
296
 
@@ -411,6 +427,12 @@ export class BrainRouter {
411
427
  })
412
428
  }
413
429
 
430
+ // Phase 32: Code intelligence — query code index for matching symbols/files
431
+ const codeTier = this.queryCodeIntelligence(query, searchProject)
432
+ if (codeTier) {
433
+ tiers.push(codeTier)
434
+ }
435
+
414
436
  const response = this.responseFilter.synthesize(tiers, message, displayProject)
415
437
 
416
438
  // P0: When no results found and message looks like a plain statement (not a question),
@@ -1052,6 +1074,12 @@ export class BrainRouter {
1052
1074
  })
1053
1075
  }
1054
1076
 
1077
+ // Phase 32: Code intelligence — synchronous SQLite query, runs alongside async searches
1078
+ const codeTier = this.queryCodeIntelligence(query, searchProject)
1079
+ if (codeTier) {
1080
+ tiers.push(codeTier)
1081
+ }
1082
+
1055
1083
  // Parallel: patterns + corrections + graph
1056
1084
  const [patternResults, correctionResults, graphResults] = await Promise.all([
1057
1085
  this.searchEngine.searchPatterns(query, { project: searchProject, limit: 3 }),
@@ -1376,6 +1404,12 @@ export class BrainRouter {
1376
1404
  })
1377
1405
  }
1378
1406
 
1407
+ // Phase 32: Code intelligence for exploration queries
1408
+ const codeTier = this.queryCodeIntelligence(query, searchProject)
1409
+ if (codeTier) {
1410
+ tiers.push(codeTier)
1411
+ }
1412
+
1379
1413
  return this.responseFilter.synthesize(tiers, message, displayProject, 'analyzed')
1380
1414
  }
1381
1415
 
@@ -1675,6 +1709,129 @@ export class BrainRouter {
1675
1709
 
1676
1710
  // ===== Helpers =====
1677
1711
 
1712
+ /**
1713
+ * Phase 32: Query code intelligence for symbols and files matching a query.
1714
+ * Returns a TierResults if matches found, null otherwise.
1715
+ * Fast operation — direct SQLite queries, no network calls.
1716
+ */
1717
+ private queryCodeIntelligence(query: string, project: string | undefined): TierResults | null {
1718
+ try {
1719
+ const codeQuery = getCodeQuery()
1720
+ if (!codeQuery) return null
1721
+
1722
+ // Extract meaningful keywords from query for code search
1723
+ const keywords = this.extractCodeKeywords(query)
1724
+ if (keywords.length === 0) return null
1725
+
1726
+ const results: FilterableResult[] = []
1727
+
1728
+ for (const keyword of keywords) {
1729
+ // Search symbols across all indexed projects (project may not match code index project name)
1730
+ const symbolResults = codeQuery.findSymbols(keyword, project || '', 5)
1731
+
1732
+ // If no results with project filter, try without (code index may use path-based project names)
1733
+ const symbols = symbolResults.length > 0
1734
+ ? symbolResults
1735
+ : codeQuery.findSymbols(keyword, '', 5)
1736
+
1737
+ for (const sym of symbols.slice(0, 3)) {
1738
+ const location = sym.lineEnd
1739
+ ? `${sym.filePath}:${sym.lineStart}-${sym.lineEnd}`
1740
+ : `${sym.filePath}:${sym.lineStart}`
1741
+ const sigPart = sym.signature ? ` — \`${sym.signature}\`` : ''
1742
+ results.push({
1743
+ content: `**${sym.symbol}** (${sym.type}) at \`${location}\`${sigPart}`,
1744
+ score: sym.confidence * 0.6, // Scale down so memories rank higher
1745
+ source: 'Code Index',
1746
+ metadata: {
1747
+ filePath: sym.filePath,
1748
+ lineStart: sym.lineStart,
1749
+ lineEnd: sym.lineEnd,
1750
+ symbolType: sym.type,
1751
+ }
1752
+ })
1753
+ }
1754
+
1755
+ // Also search files by name/summary
1756
+ const fileResults = codeQuery.findFiles(keyword, project || '')
1757
+ const files = fileResults.length > 0
1758
+ ? fileResults
1759
+ : codeQuery.findFiles(keyword, '')
1760
+
1761
+ for (const file of files.slice(0, 3)) {
1762
+ // Skip if we already have symbol results from this file
1763
+ if (results.some(r => (r.metadata as any)?.filePath === file.filePath)) continue
1764
+ const summaryPart = file.summary ? ` — ${file.summary}` : ''
1765
+ results.push({
1766
+ content: `**${file.filePath}** (${file.language || 'unknown'}, ${file.symbolCount} symbols, ${file.lineCount} lines)${summaryPart}`,
1767
+ score: 0.4, // File matches are lower confidence
1768
+ source: 'Code Index',
1769
+ metadata: {
1770
+ filePath: file.filePath,
1771
+ symbolCount: file.symbolCount,
1772
+ }
1773
+ })
1774
+ }
1775
+ }
1776
+
1777
+ // Deduplicate by filePath
1778
+ const seen = new Set<string>()
1779
+ const deduped = results.filter(r => {
1780
+ const fp = (r.metadata as any)?.filePath
1781
+ if (fp && seen.has(fp)) return false
1782
+ if (fp) seen.add(fp)
1783
+ return true
1784
+ })
1785
+
1786
+ if (deduped.length === 0) return null
1787
+
1788
+ return {
1789
+ label: 'Code Intelligence',
1790
+ results: deduped.slice(0, 5)
1791
+ }
1792
+ } catch (error) {
1793
+ this.logger.debug({ error }, 'Code intelligence query failed (non-fatal)')
1794
+ return null
1795
+ }
1796
+ }
1797
+
1798
+ /**
1799
+ * Extract meaningful keywords from a natural language query for code search.
1800
+ * Filters out stop words and common filler, returns words likely to match code symbols.
1801
+ */
1802
+ private extractCodeKeywords(query: string): string[] {
1803
+ const STOP_WORDS = new Set([
1804
+ 'how', 'what', 'where', 'when', 'why', 'who', 'which', 'are', 'is', 'was',
1805
+ 'were', 'been', 'being', 'have', 'has', 'had', 'having', 'do', 'does', 'did',
1806
+ 'doing', 'will', 'would', 'could', 'should', 'shall', 'can', 'may', 'might',
1807
+ 'must', 'the', 'a', 'an', 'and', 'but', 'or', 'nor', 'for', 'yet', 'so',
1808
+ 'in', 'on', 'at', 'to', 'from', 'by', 'with', 'about', 'into', 'through',
1809
+ 'during', 'before', 'after', 'above', 'below', 'between', 'out', 'off', 'over',
1810
+ 'under', 'again', 'further', 'then', 'once', 'here', 'there', 'all', 'each',
1811
+ 'every', 'both', 'few', 'more', 'most', 'other', 'some', 'such', 'only', 'own',
1812
+ 'same', 'than', 'too', 'very', 'just', 'because', 'not', 'this', 'that', 'these',
1813
+ 'those', 'its', 'our', 'their', 'your', 'my', 'me', 'we', 'you', 'they', 'them',
1814
+ 'know', 'tell', 'show', 'find', 'get', 'make', 'use', 'using', 'used', 'managing',
1815
+ 'managed', 'handle', 'handling', 'handled', 'work', 'working', 'works', 'implement',
1816
+ 'implementing', 'implemented', 'doing', 'done', 'project', 'code', 'file', 'function',
1817
+ ])
1818
+
1819
+ const words = query.toLowerCase()
1820
+ .replace(/[^a-z0-9\s-_]/g, ' ')
1821
+ .split(/\s+/)
1822
+ .filter(w => w.length > 2 && !STOP_WORDS.has(w))
1823
+
1824
+ // Also try multi-word phrases (camelCase/kebab-case patterns the user might reference)
1825
+ const phrases: string[] = []
1826
+ for (let i = 0; i < words.length - 1; i++) {
1827
+ // Create camelCase combo: "brain router" → "brainRouter"
1828
+ phrases.push(words[i] + words[i + 1].charAt(0).toUpperCase() + words[i + 1].slice(1))
1829
+ }
1830
+
1831
+ // Return unique keywords, single words first then phrases
1832
+ return [...new Set([...words, ...phrases])].slice(0, 5)
1833
+ }
1834
+
1678
1835
  /**
1679
1836
  * Phase 29: Link a stored observation to code files (non-blocking, fire-and-forget).
1680
1837
  */